Merge pull request #1241 from trheyi/main
Update robot member functionality with new fields and validation
This commit is contained in:
commit
3d0ae251ac
10 changed files with 531 additions and 278 deletions
284
data/bindata.go
284
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -157,7 +157,7 @@ var (
|
||||||
|
|
||||||
// DefaultMemberFields contains basic member fields
|
// DefaultMemberFields contains basic member fields
|
||||||
DefaultMemberFields = []interface{}{
|
DefaultMemberFields = []interface{}{
|
||||||
"member_id", "team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "is_owner", "status",
|
"member_id", "team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "robot_email", "role_id", "is_owner", "status",
|
||||||
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at",
|
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at",
|
||||||
"last_active_at", "login_count", "created_at", "updated_at",
|
"last_active_at", "login_count", "created_at", "updated_at",
|
||||||
}
|
}
|
||||||
|
|
@ -165,7 +165,8 @@ var (
|
||||||
// DefaultMemberDetailFields contains all member fields including robot config
|
// DefaultMemberDetailFields contains all member fields including robot config
|
||||||
DefaultMemberDetailFields = []interface{}{
|
DefaultMemberDetailFields = []interface{}{
|
||||||
"member_id", "team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "is_owner", "status",
|
"member_id", "team_id", "user_id", "member_type", "display_name", "bio", "avatar", "email", "role_id", "is_owner", "status",
|
||||||
"system_prompt", "manager_id", "robot_config", "agents", "mcp_servers",
|
"system_prompt", "manager_id", "robot_email", "authorized_senders", "email_filter_rules",
|
||||||
|
"robot_config", "agents", "mcp_servers",
|
||||||
"language_model", "cost_limit", "autonomous_mode", "last_robot_activity", "robot_status",
|
"language_model", "cost_limit", "autonomous_mode", "last_robot_activity", "robot_status",
|
||||||
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token",
|
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token",
|
||||||
"invitation_expires_at", "last_active_at",
|
"invitation_expires_at", "last_active_at",
|
||||||
|
|
|
||||||
|
|
@ -164,14 +164,13 @@ func (u *DefaultUser) MemberExists(ctx context.Context, teamID string, userID st
|
||||||
return len(members) > 0, nil
|
return len(members) > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemberExistsByTeamEmail checks if a member exists by team_id and email
|
// MemberExistsByRobotEmail checks if a robot member exists by robot_email (globally unique)
|
||||||
func (u *DefaultUser) MemberExistsByTeamEmail(ctx context.Context, teamID string, email string) (bool, error) {
|
func (u *DefaultUser) MemberExistsByRobotEmail(ctx context.Context, robotEmail string) (bool, error) {
|
||||||
m := model.Select(u.memberModel)
|
m := model.Select(u.memberModel)
|
||||||
members, err := m.Get(model.QueryParam{
|
members, err := m.Get(model.QueryParam{
|
||||||
Select: []interface{}{"id"}, // Only select ID for existence check
|
Select: []interface{}{"id"}, // Only select ID for existence check
|
||||||
Wheres: []model.QueryWhere{
|
Wheres: []model.QueryWhere{
|
||||||
{Column: "team_id", Value: teamID},
|
{Column: "robot_email", Value: robotEmail},
|
||||||
{Column: "email", Value: email},
|
|
||||||
},
|
},
|
||||||
Limit: 1,
|
Limit: 1,
|
||||||
})
|
})
|
||||||
|
|
@ -323,23 +322,22 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
|
||||||
return "", fmt.Errorf("role_id is required for robot members")
|
return "", fmt.Errorf("role_id is required for robot members")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email already exists in this team
|
// Check if robot_email already exists globally (robot_email is globally unique)
|
||||||
if email, exists := robotData["email"]; exists && email != nil && email != "" {
|
if robotEmail, exists := robotData["robot_email"]; exists && robotEmail != nil && robotEmail != "" {
|
||||||
emailStr := fmt.Sprintf("%v", email)
|
robotEmailStr := fmt.Sprintf("%v", robotEmail)
|
||||||
m := model.Select(u.memberModel)
|
m := model.Select(u.memberModel)
|
||||||
existingMembers, err := m.Get(model.QueryParam{
|
existingMembers, err := m.Get(model.QueryParam{
|
||||||
Select: []interface{}{"id"},
|
Select: []interface{}{"id"},
|
||||||
Wheres: []model.QueryWhere{
|
Wheres: []model.QueryWhere{
|
||||||
{Column: "team_id", Value: teamID},
|
{Column: "robot_email", Value: robotEmailStr},
|
||||||
{Column: "email", Value: emailStr},
|
|
||||||
},
|
},
|
||||||
Limit: 1,
|
Limit: 1,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to check email uniqueness: %w", err)
|
return "", fmt.Errorf("failed to check robot_email uniqueness: %w", err)
|
||||||
}
|
}
|
||||||
if len(existingMembers) > 0 {
|
if len(existingMembers) > 0 {
|
||||||
return "", fmt.Errorf("email %s already exists in this team", emailStr)
|
return "", fmt.Errorf("robot_email %s already exists", robotEmailStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,7 +360,8 @@ func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robo
|
||||||
|
|
||||||
// Copy robot-specific fields
|
// Copy robot-specific fields
|
||||||
robotFields := []string{
|
robotFields := []string{
|
||||||
"role_id", "system_prompt", "manager_id", "robot_config", "agents", "mcp_servers",
|
"role_id", "system_prompt", "manager_id", "robot_email", "authorized_senders", "email_filter_rules",
|
||||||
|
"robot_config", "agents", "mcp_servers",
|
||||||
"language_model", "cost_limit", "autonomous_mode", "robot_status",
|
"language_model", "cost_limit", "autonomous_mode", "robot_status",
|
||||||
"notes", "metadata",
|
"notes", "metadata",
|
||||||
"__yao_created_by", "__yao_updated_by", "__yao_team_id", "__yao_tenant_id",
|
"__yao_created_by", "__yao_updated_by", "__yao_team_id", "__yao_tenant_id",
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,15 @@ func TestRobotMemberOperations(t *testing.T) {
|
||||||
"language_model": "gpt-4",
|
"language_model": "gpt-4",
|
||||||
"cost_limit": 100.00,
|
"cost_limit": 100.00,
|
||||||
"manager_id": ownerUser,
|
"manager_id": ownerUser,
|
||||||
|
"robot_email": "testbot" + testUUID + "@robot.example.com",
|
||||||
|
"authorized_senders": []string{
|
||||||
|
"admin@example.com",
|
||||||
|
"manager@example.com",
|
||||||
|
},
|
||||||
|
"email_filter_rules": []string{
|
||||||
|
".*@example\\.com$",
|
||||||
|
".*@test\\.com$",
|
||||||
|
},
|
||||||
"robot_config": map[string]interface{}{
|
"robot_config": map[string]interface{}{
|
||||||
"max_tokens": 1000,
|
"max_tokens": 1000,
|
||||||
},
|
},
|
||||||
|
|
@ -368,6 +377,13 @@ func TestRobotMemberOperations(t *testing.T) {
|
||||||
assert.Equal(t, "robot", member["member_type"])
|
assert.Equal(t, "robot", member["member_type"])
|
||||||
assert.Equal(t, "active", member["status"]) // Robots are active by default
|
assert.Equal(t, "active", member["status"]) // Robots are active by default
|
||||||
assert.Nil(t, member["user_id"]) // Robots don't have user_id
|
assert.Nil(t, member["user_id"]) // Robots don't have user_id
|
||||||
|
|
||||||
|
// Verify new email fields in detail view
|
||||||
|
memberDetail, err := testProvider.GetMemberDetailByMemberID(ctx, robotBusinessMemberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "testbot"+testUUID+"@robot.example.com", memberDetail["robot_email"])
|
||||||
|
assert.NotNil(t, memberDetail["authorized_senders"])
|
||||||
|
assert.NotNil(t, memberDetail["email_filter_rules"])
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test GetTeamRobotMembers
|
// Test GetTeamRobotMembers
|
||||||
|
|
@ -1170,7 +1186,7 @@ func TestMemberIDOperations(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMemberExistsByTeamEmail(t *testing.T) {
|
func TestMemberExistsByRobotEmail(t *testing.T) {
|
||||||
prepare(t)
|
prepare(t)
|
||||||
defer clean()
|
defer clean()
|
||||||
|
|
||||||
|
|
@ -1184,9 +1200,9 @@ func TestMemberExistsByTeamEmail(t *testing.T) {
|
||||||
|
|
||||||
// Create test team
|
// Create test team
|
||||||
teamMap := maps.MapStrAny{
|
teamMap := maps.MapStrAny{
|
||||||
"name": "Email Test Team " + testUUID,
|
"name": "Robot Email Test Team " + testUUID,
|
||||||
"display_name": "Email Test " + testUUID,
|
"display_name": "Robot Email Test " + testUUID,
|
||||||
"description": "A test team for email testing",
|
"description": "A test team for robot email testing",
|
||||||
"owner_id": ownerUser,
|
"owner_id": ownerUser,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
}
|
}
|
||||||
|
|
@ -1194,13 +1210,13 @@ func TestMemberExistsByTeamEmail(t *testing.T) {
|
||||||
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
teamID, err := testProvider.CreateTeam(ctx, teamMap)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
testEmail := "test" + testUUID + "@example.com"
|
testRobotEmail := "testrobot" + testUUID + "@robot.example.com"
|
||||||
|
|
||||||
// Create robot member with email
|
// Create robot member with robot_email
|
||||||
t.Run("CreateRobotMemberWithEmail", func(t *testing.T) {
|
t.Run("CreateRobotMemberWithRobotEmail", func(t *testing.T) {
|
||||||
robotData := maps.MapStrAny{
|
robotData := maps.MapStrAny{
|
||||||
"display_name": "TestBot" + testUUID,
|
"display_name": "TestBot" + testUUID,
|
||||||
"email": testEmail,
|
"robot_email": testRobotEmail,
|
||||||
"role_id": "bot",
|
"role_id": "bot",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1209,19 +1225,184 @@ func TestMemberExistsByTeamEmail(t *testing.T) {
|
||||||
assert.NotEmpty(t, businessMemberID)
|
assert.NotEmpty(t, businessMemberID)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test MemberExistsByTeamEmail
|
// Test MemberExistsByRobotEmail
|
||||||
t.Run("MemberExistsByTeamEmail_Exists", func(t *testing.T) {
|
t.Run("MemberExistsByRobotEmail_Exists", func(t *testing.T) {
|
||||||
exists, err := testProvider.MemberExistsByTeamEmail(ctx, teamID, testEmail)
|
exists, err := testProvider.MemberExistsByRobotEmail(ctx, testRobotEmail)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, exists)
|
assert.True(t, exists)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test with non-existent email
|
// Test with non-existent robot email
|
||||||
t.Run("MemberExistsByTeamEmail_NotExists", func(t *testing.T) {
|
t.Run("MemberExistsByRobotEmail_NotExists", func(t *testing.T) {
|
||||||
exists, err := testProvider.MemberExistsByTeamEmail(ctx, teamID, "nonexistent@example.com")
|
exists, err := testProvider.MemberExistsByRobotEmail(ctx, "nonexistent@robot.example.com")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.False(t, exists)
|
assert.False(t, exists)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRobotEmailUniqueness(t *testing.T) {
|
||||||
|
prepare(t)
|
||||||
|
defer clean()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Use UUID to ensure unique identifiers
|
||||||
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||||
|
|
||||||
|
// Create test user (team owner)
|
||||||
|
ownerUser := createTestUser(ctx, t, "owner"+testUUID)
|
||||||
|
|
||||||
|
// Create two test teams
|
||||||
|
team1Map := maps.MapStrAny{
|
||||||
|
"name": "Robot Email Test Team 1 " + testUUID,
|
||||||
|
"display_name": "Robot Email Test 1 " + testUUID,
|
||||||
|
"description": "First test team for robot email testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
team1ID, err := testProvider.CreateTeam(ctx, team1Map)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
team2Map := maps.MapStrAny{
|
||||||
|
"name": "Robot Email Test Team 2 " + testUUID,
|
||||||
|
"display_name": "Robot Email Test 2 " + testUUID,
|
||||||
|
"description": "Second test team for robot email testing",
|
||||||
|
"owner_id": ownerUser,
|
||||||
|
"status": "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
team2ID, err := testProvider.CreateTeam(ctx, team2Map)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
testEmail := "unique-robot" + testUUID + "@robot.example.com"
|
||||||
|
|
||||||
|
// Test creating first robot with robot_email
|
||||||
|
t.Run("CreateFirstRobotWithEmail", func(t *testing.T) {
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"display_name": "Robot1" + testUUID,
|
||||||
|
"role_id": "bot",
|
||||||
|
"robot_email": testEmail,
|
||||||
|
"authorized_senders": []string{
|
||||||
|
"admin@example.com",
|
||||||
|
},
|
||||||
|
"email_filter_rules": []string{
|
||||||
|
".*@example\\.com$",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
memberID, err := testProvider.CreateRobotMember(ctx, team1ID, robotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, memberID)
|
||||||
|
|
||||||
|
// Verify robot_email was set
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, memberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, testEmail, member["robot_email"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test creating second robot with same robot_email should fail (global uniqueness)
|
||||||
|
t.Run("CreateSecondRobotWithSameEmail_ShouldFail", func(t *testing.T) {
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"display_name": "Robot2" + testUUID,
|
||||||
|
"role_id": "bot",
|
||||||
|
"robot_email": testEmail, // Same email as first robot
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := testProvider.CreateRobotMember(ctx, team2ID, robotData)
|
||||||
|
assert.Error(t, err)
|
||||||
|
// The error should indicate uniqueness constraint violation
|
||||||
|
// Note: The exact error message may vary depending on the database driver
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test creating robot with different robot_email should succeed
|
||||||
|
t.Run("CreateRobotWithDifferentEmail_ShouldSucceed", func(t *testing.T) {
|
||||||
|
differentEmail := "another-robot" + testUUID + "@robot.example.com"
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"display_name": "Robot3" + testUUID,
|
||||||
|
"role_id": "bot",
|
||||||
|
"robot_email": differentEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
memberID, err := testProvider.CreateRobotMember(ctx, team2ID, robotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, memberID)
|
||||||
|
|
||||||
|
// Verify robot_email was set
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, memberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, differentEmail, member["robot_email"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test updating robot_email
|
||||||
|
t.Run("UpdateRobotEmail", func(t *testing.T) {
|
||||||
|
// Create a new robot
|
||||||
|
newEmail := "updatable-robot" + testUUID + "@robot.example.com"
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"display_name": "Robot4" + testUUID,
|
||||||
|
"role_id": "bot",
|
||||||
|
"robot_email": newEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
memberID, err := testProvider.CreateRobotMember(ctx, team1ID, robotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Update robot_email
|
||||||
|
updatedEmail := "updated-robot" + testUUID + "@robot.example.com"
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"robot_email": updatedEmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = testProvider.UpdateMemberByMemberID(ctx, memberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, memberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, updatedEmail, member["robot_email"])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test updating authorized_senders and email_filter_rules
|
||||||
|
t.Run("UpdateRobotEmailConfiguration", func(t *testing.T) {
|
||||||
|
// Create a new robot
|
||||||
|
robotData := maps.MapStrAny{
|
||||||
|
"display_name": "Robot5" + testUUID,
|
||||||
|
"role_id": "bot",
|
||||||
|
"robot_email": "config-robot" + testUUID + "@robot.example.com",
|
||||||
|
"authorized_senders": []string{
|
||||||
|
"initial@example.com",
|
||||||
|
},
|
||||||
|
"email_filter_rules": []string{
|
||||||
|
".*@initial\\.com$",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
memberID, err := testProvider.CreateRobotMember(ctx, team1ID, robotData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Update email configuration
|
||||||
|
updateData := maps.MapStrAny{
|
||||||
|
"authorized_senders": []string{
|
||||||
|
"admin@example.com",
|
||||||
|
"manager@example.com",
|
||||||
|
"owner@example.com",
|
||||||
|
},
|
||||||
|
"email_filter_rules": []string{
|
||||||
|
".*@example\\.com$",
|
||||||
|
".*@test\\.com$",
|
||||||
|
".*@company\\.com$",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = testProvider.UpdateMemberByMemberID(ctx, memberID, updateData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
member, err := testProvider.GetMemberDetailByMemberID(ctx, memberID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, member["authorized_senders"])
|
||||||
|
assert.NotNil(t, member["email_filter_rules"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function createTestUser is defined in team_test.go
|
// Helper function createTestUser is defined in team_test.go
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,7 @@ type UserProvider interface {
|
||||||
GetMemberDetailByMemberID(ctx context.Context, memberID string) (maps.MapStrAny, error)
|
GetMemberDetailByMemberID(ctx context.Context, memberID string) (maps.MapStrAny, error)
|
||||||
GetMemberByInvitationID(ctx context.Context, invitationID string) (maps.MapStrAny, error)
|
GetMemberByInvitationID(ctx context.Context, invitationID string) (maps.MapStrAny, error)
|
||||||
MemberExists(ctx context.Context, teamID string, userID string) (bool, error)
|
MemberExists(ctx context.Context, teamID string, userID string) (bool, error)
|
||||||
MemberExistsByTeamEmail(ctx context.Context, teamID string, email string) (bool, error)
|
MemberExistsByRobotEmail(ctx context.Context, robotEmail string) (bool, error)
|
||||||
CreateMember(ctx context.Context, memberData maps.MapStrAny) (string, error)
|
CreateMember(ctx context.Context, memberData maps.MapStrAny) (string, error)
|
||||||
UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error
|
UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error
|
||||||
UpdateMemberByID(ctx context.Context, memberID int64, memberData maps.MapStrAny) error
|
UpdateMemberByID(ctx context.Context, memberID int64, memberData maps.MapStrAny) error
|
||||||
|
|
|
||||||
|
|
@ -1136,17 +1136,20 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
"create robot with all fields",
|
"create robot with all fields",
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "AI Assistant Full",
|
"name": "AI Assistant Full",
|
||||||
"email": fmt.Sprintf("ai-full-%s@test.com", testUUID),
|
"email": fmt.Sprintf("ai-full-%s@test.com", testUUID),
|
||||||
"bio": "A comprehensive AI assistant",
|
"robot_email": fmt.Sprintf("robot-full-%s@robot.test.com", testUUID),
|
||||||
"role": "member",
|
"authorized_senders": []string{"user1@test.com", "user2@test.com"},
|
||||||
"report_to": tokenInfo.UserID,
|
"email_filter_rules": []string{".*@company\\.com", ".*@partner\\.com"},
|
||||||
"prompt": "You are a helpful AI assistant with full capabilities",
|
"bio": "A comprehensive AI assistant",
|
||||||
"llm": "gpt-4",
|
"role": "member",
|
||||||
"agents": []string{"data-analyst", "code-reviewer"},
|
"report_to": tokenInfo.UserID,
|
||||||
"mcp_tools": []string{"filesystem", "database"},
|
"prompt": "You are a helpful AI assistant with full capabilities",
|
||||||
"autonomous_mode": "enabled",
|
"llm": "gpt-4",
|
||||||
"cost_limit": 100.50,
|
"agents": []string{"data-analyst", "code-reviewer"},
|
||||||
|
"mcp_tools": []string{"filesystem", "database"},
|
||||||
|
"autonomous_mode": "enabled",
|
||||||
|
"cost_limit": 100.50,
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
|
@ -1158,10 +1161,10 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
"create robot with required fields only",
|
"create robot with required fields only",
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "AI Assistant Min",
|
"name": "AI Assistant Min",
|
||||||
"email": fmt.Sprintf("ai-min-%s@test.com", testUUID),
|
"robot_email": fmt.Sprintf("ai-min-%s@test.com", testUUID),
|
||||||
"role": "member",
|
"role": "member",
|
||||||
"prompt": "You are a basic assistant",
|
"prompt": "You are a basic assistant",
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
|
@ -1174,7 +1177,7 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "AI Assistant Auto",
|
"name": "AI Assistant Auto",
|
||||||
"email": fmt.Sprintf("ai-auto-%s@test.com", testUUID),
|
"robot_email": fmt.Sprintf("ai-auto-%s@test.com", testUUID),
|
||||||
"role": "member",
|
"role": "member",
|
||||||
"prompt": "You are an autonomous assistant",
|
"prompt": "You are an autonomous assistant",
|
||||||
"autonomous_mode": "1", // Test numeric string
|
"autonomous_mode": "1", // Test numeric string
|
||||||
|
|
@ -1190,7 +1193,7 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "AI Assistant Manual",
|
"name": "AI Assistant Manual",
|
||||||
"email": fmt.Sprintf("ai-manual-%s@test.com", testUUID),
|
"robot_email": fmt.Sprintf("ai-manual-%s@test.com", testUUID),
|
||||||
"role": "member",
|
"role": "member",
|
||||||
"prompt": "You are a manual assistant",
|
"prompt": "You are a manual assistant",
|
||||||
"autonomous_mode": "disabled",
|
"autonomous_mode": "disabled",
|
||||||
|
|
@ -1205,9 +1208,9 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
"create robot without name",
|
"create robot without name",
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"email": "no-name@test.com",
|
"robot_email": "no-name@test.com",
|
||||||
"role": "member",
|
"role": "member",
|
||||||
"prompt": "You are an assistant",
|
"prompt": "You are an assistant",
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
|
@ -1216,10 +1219,10 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
"should require name",
|
"should require name",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create robot without email",
|
"create robot without robot_email",
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "No Email Robot",
|
"name": "No Robot Email Robot",
|
||||||
"role": "member",
|
"role": "member",
|
||||||
"prompt": "You are an assistant",
|
"prompt": "You are an assistant",
|
||||||
},
|
},
|
||||||
|
|
@ -1227,15 +1230,15 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
},
|
},
|
||||||
400,
|
400,
|
||||||
"should require email",
|
"should require robot_email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create robot without role",
|
"create robot without role",
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "No Role Robot",
|
"name": "No Role Robot",
|
||||||
"email": "no-role@test.com",
|
"robot_email": "no-role@test.com",
|
||||||
"prompt": "You are an assistant",
|
"prompt": "You are an assistant",
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
|
@ -1247,9 +1250,9 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
"create robot without prompt",
|
"create robot without prompt",
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "No Prompt Robot",
|
"name": "No Prompt Robot",
|
||||||
"email": "no-prompt@test.com",
|
"robot_email": "no-prompt@test.com",
|
||||||
"role": "member",
|
"role": "member",
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
|
@ -1258,28 +1261,29 @@ func TestMemberCreateRobot(t *testing.T) {
|
||||||
"should require prompt",
|
"should require prompt",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create robot with duplicate email",
|
"create robot with duplicate robot_email",
|
||||||
teamID,
|
teamID,
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "Duplicate Email Robot",
|
"name": "Duplicate Robot Email Robot",
|
||||||
"email": fmt.Sprintf("ai-full-%s@test.com", testUUID), // Same as first successful case
|
"email": fmt.Sprintf("duplicate-robot-%s@test.com", testUUID), // Different email
|
||||||
"role": "member",
|
"robot_email": fmt.Sprintf("robot-full-%s@robot.test.com", testUUID), // Same robot_email as first successful case
|
||||||
"prompt": "You are an assistant",
|
"role": "member",
|
||||||
|
"prompt": "You are an assistant",
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
},
|
},
|
||||||
409,
|
409,
|
||||||
"should reject duplicate email in same team",
|
"should reject duplicate robot_email globally",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create robot in non-existent team",
|
"create robot in non-existent team",
|
||||||
"non-existent-team-id",
|
"non-existent-team-id",
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"name": "Robot in Void",
|
"name": "Robot in Void",
|
||||||
"email": "void@test.com",
|
"robot_email": "void@test.com",
|
||||||
"role": "member",
|
"role": "member",
|
||||||
"prompt": "You are lost",
|
"prompt": "You are lost",
|
||||||
},
|
},
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
|
|
@ -1394,8 +1398,8 @@ func toString(v interface{}) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMemberCheckEmail tests the GET /user/teams/:team_id/members/check endpoint
|
// TestMemberCheckRobotEmail tests the GET /user/teams/:team_id/members/check-robot-email endpoint
|
||||||
func TestMemberCheckEmail(t *testing.T) {
|
func TestMemberCheckRobotEmail(t *testing.T) {
|
||||||
// Initialize test environment
|
// Initialize test environment
|
||||||
serverURL := testutils.Prepare(t)
|
serverURL := testutils.Prepare(t)
|
||||||
defer testutils.Clean()
|
defer testutils.Clean()
|
||||||
|
|
@ -1407,7 +1411,7 @@ func TestMemberCheckEmail(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register a test client for OAuth authentication
|
// Register a test client for OAuth authentication
|
||||||
testClient := testutils.RegisterTestClient(t, "Member Check Email Test Client", []string{"https://localhost/callback"})
|
testClient := testutils.RegisterTestClient(t, "Member Check Robot Email Test Client", []string{"https://localhost/callback"})
|
||||||
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
defer testutils.CleanupTestClient(t, testClient.ClientID)
|
||||||
|
|
||||||
// Obtain access token for authenticated requests
|
// Obtain access token for authenticated requests
|
||||||
|
|
@ -1417,16 +1421,17 @@ func TestMemberCheckEmail(t *testing.T) {
|
||||||
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
testUUID := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
|
||||||
|
|
||||||
// Create a test team
|
// Create a test team
|
||||||
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Email Check Test Team "+testUUID)
|
createdTeam := createTestTeam(t, serverURL, baseURL, tokenInfo.AccessToken, "Robot Email Check Test Team "+testUUID)
|
||||||
teamID := getTeamID(createdTeam)
|
teamID := getTeamID(createdTeam)
|
||||||
|
|
||||||
// Create a robot member with a known email
|
// Create a robot member with a known robot_email (globally unique)
|
||||||
existingEmail := fmt.Sprintf("existing-robot-%s@test.com", testUUID)
|
existingRobotEmail := fmt.Sprintf("existing-robot-%s@robot.test.com", testUUID)
|
||||||
robotBody := map[string]interface{}{
|
robotBody := map[string]interface{}{
|
||||||
"name": "Existing Robot",
|
"name": "Existing Robot",
|
||||||
"email": existingEmail,
|
"email": fmt.Sprintf("display-%s@test.com", testUUID), // Display email (can be non-unique)
|
||||||
"role": "member",
|
"robot_email": existingRobotEmail, // Globally unique robot email
|
||||||
"prompt": "You are a test robot",
|
"role": "member",
|
||||||
|
"prompt": "You are a test robot",
|
||||||
}
|
}
|
||||||
robotBodyBytes, _ := json.Marshal(robotBody)
|
robotBodyBytes, _ := json.Marshal(robotBody)
|
||||||
robotReq, _ := http.NewRequest("POST", serverURL+baseURL+"/user/teams/"+teamID+"/members/robots", bytes.NewBuffer(robotBodyBytes))
|
robotReq, _ := http.NewRequest("POST", serverURL+baseURL+"/user/teams/"+teamID+"/members/robots", bytes.NewBuffer(robotBodyBytes))
|
||||||
|
|
@ -1443,45 +1448,45 @@ func TestMemberCheckEmail(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
teamID string
|
teamID string
|
||||||
email string
|
robotEmail string
|
||||||
headers map[string]string
|
headers map[string]string
|
||||||
expectCode int
|
expectCode int
|
||||||
expectExists bool
|
expectExists bool
|
||||||
expectMsg string
|
expectMsg string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"check email without authentication",
|
"check robot email without authentication",
|
||||||
teamID,
|
teamID,
|
||||||
existingEmail,
|
existingRobotEmail,
|
||||||
map[string]string{},
|
map[string]string{},
|
||||||
401,
|
401,
|
||||||
false,
|
false,
|
||||||
"should require authentication",
|
"should require authentication",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"check existing email",
|
"check existing robot email",
|
||||||
teamID,
|
teamID,
|
||||||
existingEmail,
|
existingRobotEmail,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
true,
|
true,
|
||||||
"should return exists=true for existing email",
|
"should return exists=true for existing robot email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"check non-existing email",
|
"check non-existing robot email",
|
||||||
teamID,
|
teamID,
|
||||||
fmt.Sprintf("nonexistent-%s@test.com", testUUID),
|
fmt.Sprintf("nonexistent-%s@robot.test.com", testUUID),
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
false,
|
false,
|
||||||
"should return exists=false for non-existing email",
|
"should return exists=false for non-existing robot email",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"check email without email parameter",
|
"check robot email without robot_email parameter",
|
||||||
teamID,
|
teamID,
|
||||||
"",
|
"",
|
||||||
map[string]string{
|
map[string]string{
|
||||||
|
|
@ -1489,12 +1494,12 @@ func TestMemberCheckEmail(t *testing.T) {
|
||||||
},
|
},
|
||||||
400,
|
400,
|
||||||
false,
|
false,
|
||||||
"should require email parameter",
|
"should require robot_email parameter",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"check email in non-existent team",
|
"check robot email in non-existent team",
|
||||||
"non-existent-team-id",
|
"non-existent-team-id",
|
||||||
"test@example.com",
|
"test@robot.example.com",
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
"Authorization": "Bearer " + tokenInfo.AccessToken,
|
||||||
},
|
},
|
||||||
|
|
@ -1506,9 +1511,9 @@ func TestMemberCheckEmail(t *testing.T) {
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/check"
|
requestURL := serverURL + baseURL + "/user/teams/" + tc.teamID + "/members/check-robot-email"
|
||||||
if tc.email != "" {
|
if tc.robotEmail != "" {
|
||||||
requestURL += "?email=" + tc.email
|
requestURL += "?robot_email=" + tc.robotEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", requestURL, nil)
|
req, err := http.NewRequest("GET", requestURL, nil)
|
||||||
|
|
@ -1535,15 +1540,13 @@ func TestMemberCheckEmail(t *testing.T) {
|
||||||
err = json.Unmarshal(body, &response)
|
err = json.Unmarshal(body, &response)
|
||||||
assert.NoError(t, err, "Should parse JSON response")
|
assert.NoError(t, err, "Should parse JSON response")
|
||||||
|
|
||||||
// Verify response structure
|
// Verify response structure (global check, no team_id in response)
|
||||||
assert.Contains(t, response, "exists", "Should have exists field")
|
assert.Contains(t, response, "exists", "Should have exists field")
|
||||||
assert.Contains(t, response, "email", "Should have email field")
|
assert.Contains(t, response, "robot_email", "Should have robot_email field")
|
||||||
assert.Contains(t, response, "team_id", "Should have team_id field")
|
|
||||||
|
|
||||||
// Verify values
|
// Verify values
|
||||||
assert.Equal(t, tc.expectExists, response["exists"], "Should have correct exists value")
|
assert.Equal(t, tc.expectExists, response["exists"], "Should have correct exists value")
|
||||||
assert.Equal(t, tc.email, response["email"], "Should have correct email")
|
assert.Equal(t, tc.robotEmail, response["robot_email"], "Should have correct robot_email")
|
||||||
assert.Equal(t, teamID, response["team_id"], "Should have correct team_id")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Member check email test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
t.Logf("Member check email test %s: status=%d, body=%s", tc.name, resp.StatusCode, string(body))
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,8 @@ func GinMemberList(c *gin.Context) {
|
||||||
response.RespondWithSuccess(c, http.StatusOK, result)
|
response.RespondWithSuccess(c, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GinMemberCheckEmail handles GET /api/user/teams/:id/members/check?email=xxx - Check if email exists in team
|
// GinMemberCheckRobotEmail handles GET /api/user/teams/:id/members/check-robot-email?robot_email=xxx - Check if robot email exists globally
|
||||||
func GinMemberCheckEmail(c *gin.Context) {
|
func GinMemberCheckRobotEmail(c *gin.Context) {
|
||||||
// Get authorized user info
|
// Get authorized user info
|
||||||
authInfo := oauth.GetAuthorizedInfo(c)
|
authInfo := oauth.GetAuthorizedInfo(c)
|
||||||
if authInfo == nil || authInfo.UserID == "" {
|
if authInfo == nil || authInfo.UserID == "" {
|
||||||
|
|
@ -141,20 +141,20 @@ func GinMemberCheckEmail(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
email := c.Query("email")
|
robotEmail := c.Query("robot_email")
|
||||||
if email == "" {
|
if robotEmail == "" {
|
||||||
errorResp := &response.ErrorResponse{
|
errorResp := &response.ErrorResponse{
|
||||||
Code: response.ErrInvalidRequest.Code,
|
Code: response.ErrInvalidRequest.Code,
|
||||||
ErrorDescription: "Email query parameter is required",
|
ErrorDescription: "robot_email query parameter is required",
|
||||||
}
|
}
|
||||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call business logic
|
// Call business logic
|
||||||
exists, err := memberCheckEmail(c.Request.Context(), authInfo.UserID, teamID, email)
|
exists, err := memberCheckRobotEmail(c.Request.Context(), authInfo.UserID, teamID, robotEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to check member email: %v", err)
|
log.Error("Failed to check robot email: %v", err)
|
||||||
// Check error type for appropriate response
|
// Check error type for appropriate response
|
||||||
if strings.Contains(err.Error(), "not found") {
|
if strings.Contains(err.Error(), "not found") {
|
||||||
errorResp := &response.ErrorResponse{
|
errorResp := &response.ErrorResponse{
|
||||||
|
|
@ -171,7 +171,7 @@ func GinMemberCheckEmail(c *gin.Context) {
|
||||||
} else {
|
} else {
|
||||||
errorResp := &response.ErrorResponse{
|
errorResp := &response.ErrorResponse{
|
||||||
Code: response.ErrServerError.Code,
|
Code: response.ErrServerError.Code,
|
||||||
ErrorDescription: fmt.Sprintf("Failed to check member email: %v", err),
|
ErrorDescription: fmt.Sprintf("Failed to check robot email: %v", err),
|
||||||
}
|
}
|
||||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||||
}
|
}
|
||||||
|
|
@ -180,9 +180,8 @@ func GinMemberCheckEmail(c *gin.Context) {
|
||||||
|
|
||||||
// Return result
|
// Return result
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"exists": exists,
|
"exists": exists,
|
||||||
"email": email,
|
"robot_email": robotEmail,
|
||||||
"team_id": teamID,
|
|
||||||
}
|
}
|
||||||
response.RespondWithSuccess(c, http.StatusOK, result)
|
response.RespondWithSuccess(c, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
@ -280,7 +279,7 @@ func GinMemberCreateRobot(c *gin.Context) {
|
||||||
// Prepare base robot member data
|
// Prepare base robot member data
|
||||||
baseData := maps.MapStrAny{
|
baseData := maps.MapStrAny{
|
||||||
"display_name": req.Name,
|
"display_name": req.Name,
|
||||||
"email": req.Email,
|
"robot_email": req.RobotEmail, // Required: globally unique email
|
||||||
"bio": req.Bio,
|
"bio": req.Bio,
|
||||||
"role_id": req.RoleID,
|
"role_id": req.RoleID,
|
||||||
"system_prompt": req.SystemPrompt,
|
"system_prompt": req.SystemPrompt,
|
||||||
|
|
@ -288,6 +287,15 @@ func GinMemberCreateRobot(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add optional fields
|
// Add optional fields
|
||||||
|
if req.Email != "" {
|
||||||
|
baseData["email"] = req.Email // Optional: display-only email
|
||||||
|
}
|
||||||
|
if len(req.AuthorizedSenders) > 0 {
|
||||||
|
baseData["authorized_senders"] = req.AuthorizedSenders
|
||||||
|
}
|
||||||
|
if len(req.EmailFilterRules) > 0 {
|
||||||
|
baseData["email_filter_rules"] = req.EmailFilterRules
|
||||||
|
}
|
||||||
if req.ManagerID != "" {
|
if req.ManagerID != "" {
|
||||||
baseData["manager_id"] = req.ManagerID
|
baseData["manager_id"] = req.ManagerID
|
||||||
}
|
}
|
||||||
|
|
@ -857,8 +865,8 @@ func memberGet(ctx context.Context, userID, teamID, memberID string) (maps.MapSt
|
||||||
return memberData, nil
|
return memberData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// memberCheckEmail handles the business logic for checking if member exists by team_id and email
|
// memberCheckRobotEmail handles the business logic for checking if robot email exists globally
|
||||||
func memberCheckEmail(ctx context.Context, userID, teamID, email string) (bool, error) {
|
func memberCheckRobotEmail(ctx context.Context, userID, teamID, robotEmail string) (bool, error) {
|
||||||
// Check if user has access to the team (read permission: owner or member)
|
// Check if user has access to the team (read permission: owner or member)
|
||||||
isOwner, isMember, err := checkTeamAccess(ctx, teamID, userID)
|
isOwner, isMember, err := checkTeamAccess(ctx, teamID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -876,10 +884,10 @@ func memberCheckEmail(ctx context.Context, userID, teamID, email string) (bool,
|
||||||
return false, fmt.Errorf("failed to get user provider: %w", err)
|
return false, fmt.Errorf("failed to get user provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if member exists by team_id and email
|
// Check if robot email exists globally (not limited to team)
|
||||||
exists, err := provider.MemberExistsByTeamEmail(ctx, teamID, email)
|
exists, err := provider.MemberExistsByRobotEmail(ctx, robotEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("failed to check member existence: %w", err)
|
return false, fmt.Errorf("failed to check robot email existence: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return exists, nil
|
return exists, nil
|
||||||
|
|
@ -1011,6 +1019,7 @@ func mapToMemberResponse(data maps.MapStr) MemberResponse {
|
||||||
Bio: toString(data["bio"]),
|
Bio: toString(data["bio"]),
|
||||||
Avatar: toString(data["avatar"]),
|
Avatar: toString(data["avatar"]),
|
||||||
Email: toString(data["email"]),
|
Email: toString(data["email"]),
|
||||||
|
RobotEmail: toString(data["robot_email"]), // Globally unique email for robot members
|
||||||
RoleID: toString(data["role_id"]),
|
RoleID: toString(data["role_id"]),
|
||||||
IsOwner: data["is_owner"], // Keep original type (int or bool)
|
IsOwner: data["is_owner"], // Keep original type (int or bool)
|
||||||
Status: toString(data["status"]),
|
Status: toString(data["status"]),
|
||||||
|
|
@ -1071,6 +1080,36 @@ func mapToMemberDetailResponse(data maps.MapStr) MemberDetailResponse {
|
||||||
Notes: toString(data["notes"]),
|
Notes: toString(data["notes"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle authorized_senders array
|
||||||
|
if authorizedSenders, ok := data["authorized_senders"]; ok {
|
||||||
|
if sendersSlice, ok := authorizedSenders.([]interface{}); ok {
|
||||||
|
sendersList := make([]string, 0, len(sendersSlice))
|
||||||
|
for _, s := range sendersSlice {
|
||||||
|
if senderStr, ok := s.(string); ok {
|
||||||
|
sendersList = append(sendersList, senderStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
member.AuthorizedSenders = sendersList
|
||||||
|
} else if sendersStrSlice, ok := authorizedSenders.([]string); ok {
|
||||||
|
member.AuthorizedSenders = sendersStrSlice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle email_filter_rules array
|
||||||
|
if filterRules, ok := data["email_filter_rules"]; ok {
|
||||||
|
if rulesSlice, ok := filterRules.([]interface{}); ok {
|
||||||
|
rulesList := make([]string, 0, len(rulesSlice))
|
||||||
|
for _, r := range rulesSlice {
|
||||||
|
if ruleStr, ok := r.(string); ok {
|
||||||
|
rulesList = append(rulesList, ruleStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
member.EmailFilterRules = rulesList
|
||||||
|
} else if rulesStrSlice, ok := filterRules.([]string); ok {
|
||||||
|
member.EmailFilterRules = rulesStrSlice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle robot_config map
|
// Handle robot_config map
|
||||||
if robotConfig, ok := data["robot_config"]; ok {
|
if robotConfig, ok := data["robot_config"]; ok {
|
||||||
if configMap, ok := robotConfig.(map[string]interface{}); ok {
|
if configMap, ok := robotConfig.(map[string]interface{}); ok {
|
||||||
|
|
|
||||||
|
|
@ -403,6 +403,7 @@ type MemberResponse struct {
|
||||||
Bio string `json:"bio,omitempty"`
|
Bio string `json:"bio,omitempty"`
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
|
RobotEmail string `json:"robot_email,omitempty"` // Globally unique email for robot members
|
||||||
RoleID string `json:"role_id"`
|
RoleID string `json:"role_id"`
|
||||||
IsOwner interface{} `json:"is_owner,omitempty"` // Can be int or bool
|
IsOwner interface{} `json:"is_owner,omitempty"` // Can be int or bool
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
|
@ -425,6 +426,8 @@ type MemberDetailResponse struct {
|
||||||
// Robot-specific fields (only for robot members)
|
// Robot-specific fields (only for robot members)
|
||||||
SystemPrompt string `json:"system_prompt,omitempty"`
|
SystemPrompt string `json:"system_prompt,omitempty"`
|
||||||
ManagerID string `json:"manager_id,omitempty"`
|
ManagerID string `json:"manager_id,omitempty"`
|
||||||
|
AuthorizedSenders []string `json:"authorized_senders,omitempty"` // Whitelist of emails authorized to send commands
|
||||||
|
EmailFilterRules []string `json:"email_filter_rules,omitempty"` // Email filtering rules (supports regex patterns)
|
||||||
RobotConfig map[string]interface{} `json:"robot_config,omitempty"`
|
RobotConfig map[string]interface{} `json:"robot_config,omitempty"`
|
||||||
Agents []string `json:"agents,omitempty"`
|
Agents []string `json:"agents,omitempty"`
|
||||||
MCPServers []string `json:"mcp_servers,omitempty"`
|
MCPServers []string `json:"mcp_servers,omitempty"`
|
||||||
|
|
@ -441,17 +444,20 @@ type MemberDetailResponse struct {
|
||||||
|
|
||||||
// CreateRobotMemberRequest represents the request to create a new robot member
|
// CreateRobotMemberRequest represents the request to create a new robot member
|
||||||
type CreateRobotMemberRequest struct {
|
type CreateRobotMemberRequest struct {
|
||||||
Name string `json:"name" binding:"required"` // Display name
|
Name string `json:"name" binding:"required"` // Display name
|
||||||
Email string `json:"email" binding:"required"` // Email address
|
Email string `json:"email,omitempty"` // Email address (optional, for display only)
|
||||||
Bio string `json:"bio,omitempty"` // Bio/description
|
RobotEmail string `json:"robot_email" binding:"required"` // Robot's globally unique email address (required)
|
||||||
RoleID string `json:"role" binding:"required"` // Role ID
|
AuthorizedSenders []string `json:"authorized_senders,omitempty"` // Whitelist of emails authorized to send commands
|
||||||
ManagerID string `json:"report_to,omitempty"` // Direct manager user ID
|
EmailFilterRules []string `json:"email_filter_rules,omitempty"` // Email filtering rules (supports regex patterns)
|
||||||
SystemPrompt string `json:"prompt" binding:"required"` // Identity & role prompt
|
Bio string `json:"bio,omitempty"` // Bio/description
|
||||||
LanguageModel string `json:"llm,omitempty"` // Language model (e.g., "gpt-4")
|
RoleID string `json:"role" binding:"required"` // Role ID
|
||||||
Agents []string `json:"agents,omitempty"` // Accessible agents
|
ManagerID string `json:"report_to,omitempty"` // Direct manager user ID
|
||||||
MCPServers []string `json:"mcp_tools,omitempty"` // MCP servers/tools
|
SystemPrompt string `json:"prompt" binding:"required"` // Identity & role prompt
|
||||||
AutonomousMode string `json:"autonomous_mode,omitempty"` // "enabled" or "disabled"
|
LanguageModel string `json:"llm,omitempty"` // Language model (e.g., "gpt-4")
|
||||||
CostLimit float64 `json:"cost_limit,omitempty"` // Monthly cost limit in USD
|
Agents []string `json:"agents,omitempty"` // Accessible agents
|
||||||
|
MCPServers []string `json:"mcp_tools,omitempty"` // MCP servers/tools
|
||||||
|
AutonomousMode string `json:"autonomous_mode,omitempty"` // "enabled" or "disabled"
|
||||||
|
CostLimit float64 `json:"cost_limit,omitempty"` // Monthly cost limit in USD
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemberListRequest represents the request to list team members with advanced filtering
|
// MemberListRequest represents the request to list team members with advanced filtering
|
||||||
|
|
|
||||||
|
|
@ -133,12 +133,12 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
||||||
team.GET("/current", GinTeamCurrent)
|
team.GET("/current", GinTeamCurrent)
|
||||||
|
|
||||||
// Team Members - Nested resource endpoints
|
// Team Members - Nested resource endpoints
|
||||||
team.GET("/:id/members", GinMemberList) // GET /api/user/teams/:id/members - List team members
|
team.GET("/:id/members", GinMemberList) // GET /api/user/teams/:id/members - List team members
|
||||||
team.GET("/:id/members/check", GinMemberCheckEmail) // GET /api/user/teams/:id/members/check?email=xxx - Check if email exists in team
|
team.GET("/:id/members/check-robot-email", GinMemberCheckRobotEmail) // GET /api/user/teams/:id/members/check-robot-email?robot_email=xxx - Check if robot email exists globally
|
||||||
team.POST("/:id/members/robots", GinMemberCreateRobot) // POST /api/user/teams/:id/members/robots - Add robot member
|
team.POST("/:id/members/robots", GinMemberCreateRobot) // POST /api/user/teams/:id/members/robots - Add robot member
|
||||||
team.GET("/:id/members/:member_id", GinMemberGet) // GET /api/user/teams/:id/members/:member_id - Get member details
|
team.GET("/:id/members/:member_id", GinMemberGet) // GET /api/user/teams/:id/members/:member_id - Get member details
|
||||||
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /api/user/teams/:id/members/:member_id - Update member
|
team.PUT("/:id/members/:member_id", GinMemberUpdate) // PUT /api/user/teams/:id/members/:member_id - Update member
|
||||||
team.DELETE("/:id/members/:member_id", GinMemberDelete) // DELETE /api/user/teams/:id/members/:member_id - Remove member
|
team.DELETE("/:id/members/:member_id", GinMemberDelete) // DELETE /api/user/teams/:id/members/:member_id - Remove member
|
||||||
|
|
||||||
// Team Invitations - Nested resource endpoints
|
// Team Invitations - Nested resource endpoints
|
||||||
team.GET("/:id/invitations", GinTeamInvitationList) // GET /teams/:id/invitations - List invitations
|
team.GET("/:id/invitations", GinTeamInvitationList) // GET /teams/:id/invitations - List invitations
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,30 @@
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Robot Configuration Fields (only for robot members)
|
// Robot Configuration Fields (only for robot members)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
{
|
||||||
|
"name": "robot_email",
|
||||||
|
"type": "string",
|
||||||
|
"label": "Robot Email",
|
||||||
|
"comment": "Globally unique email address for the robot to receive and send emails",
|
||||||
|
"length": 255,
|
||||||
|
"nullable": true,
|
||||||
|
"unique": true,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "authorized_senders",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Authorized Senders",
|
||||||
|
"comment": "Whitelist of email addresses authorized to send instructions to this robot. Robot will respond to and execute commands from these senders only (JSON array)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email_filter_rules",
|
||||||
|
"type": "json",
|
||||||
|
"label": "Email Filter Rules",
|
||||||
|
"comment": "Email filtering rules (supports regex patterns) to determine which emails to receive and process. Can include domain patterns, address patterns, etc. (JSON array)",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "robot_config",
|
"name": "robot_config",
|
||||||
"type": "json",
|
"type": "json",
|
||||||
|
|
@ -337,10 +361,10 @@
|
||||||
"comment": "Unique constraint: one user can have only one membership per team, with unique invitation_id for pending invitations"
|
"comment": "Unique constraint: one user can have only one membership per team, with unique invitation_id for pending invitations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "idx_team_email_unique",
|
"name": "idx_team_email",
|
||||||
"columns": ["team_id", "email"],
|
"columns": ["team_id", "email"],
|
||||||
"type": "unique",
|
"type": "index",
|
||||||
"comment": "Unique constraint: one email per team (used for communication, applies to both users and robots)"
|
"comment": "Index for querying members by email within team (email is for display only)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "idx_team_member_type_role",
|
"name": "idx_team_member_type_role",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue