- Added the `UpdateRobotMember` method to handle updates for robot members, including validation for member existence, type checks, and email uniqueness. - Introduced a new `UpdateRobotMemberRequest` structure to facilitate updates via the API, allowing for modifications to various fields such as `robot_email`, `status`, and `autonomous_mode`. - Enhanced the `GinMemberUpdateRobot` handler to process update requests, ensuring proper authentication and error handling for various scenarios. - Expanded test coverage with new test cases for the robot member update functionality, validating successful updates, error conditions, and edge cases. - Refactored existing member management logic to integrate the new update capabilities, improving overall system robustness and user experience.
1100 lines
32 KiB
Go
1100 lines
32 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/yaoapp/gou/model"
|
|
"github.com/yaoapp/kun/maps"
|
|
)
|
|
|
|
// Member Resource
|
|
|
|
// GetMember retrieves member information by team_id and user_id
|
|
func (u *DefaultUser) GetMember(ctx context.Context, teamID string, userID string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: u.memberFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return members[0], nil
|
|
}
|
|
|
|
// GetMemberDetail retrieves detailed member information
|
|
func (u *DefaultUser) GetMemberDetail(ctx context.Context, teamID string, userID string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: u.memberDetailFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return members[0], nil
|
|
}
|
|
|
|
// GetMemberByID retrieves member information by internal ID
|
|
func (u *DefaultUser) GetMemberByID(ctx context.Context, memberID int64) (maps.MapStrAny, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: u.memberFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "id", Value: memberID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return members[0], nil
|
|
}
|
|
|
|
// GetMemberByInvitationID retrieves member information by invitation_id
|
|
func (u *DefaultUser) GetMemberByInvitationID(ctx context.Context, invitationID string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: u.memberFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "invitation_id", Value: invitationID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return members[0], nil
|
|
}
|
|
|
|
// GetMemberByMemberID retrieves member information by member_id (business ID)
|
|
func (u *DefaultUser) GetMemberByMemberID(ctx context.Context, memberID string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: u.memberFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "member_id", Value: memberID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return members[0], nil
|
|
}
|
|
|
|
// GetMemberDetailByMemberID retrieves detailed member information by member_id (business ID)
|
|
func (u *DefaultUser) GetMemberDetailByMemberID(ctx context.Context, memberID string) (maps.MapStrAny, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: u.memberDetailFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "member_id", Value: memberID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return nil, fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return members[0], nil
|
|
}
|
|
|
|
// MemberExists checks if a member exists by team_id and user_id
|
|
func (u *DefaultUser) MemberExists(ctx context.Context, teamID string, userID string) (bool, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id"}, // Only select ID for existence check
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return len(members) > 0, nil
|
|
}
|
|
|
|
// MemberExistsByRobotEmail checks if a robot member exists by robot_email (globally unique)
|
|
func (u *DefaultUser) MemberExistsByRobotEmail(ctx context.Context, robotEmail string) (bool, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id"}, // Only select ID for existence check
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "robot_email", Value: robotEmail},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return len(members) > 0, nil
|
|
}
|
|
|
|
// MemberExistsByMemberID checks if a member exists by member_id (business ID)
|
|
func (u *DefaultUser) MemberExistsByMemberID(ctx context.Context, memberID string) (bool, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id"}, // Only select ID for existence check
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "member_id", Value: memberID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return len(members) > 0, nil
|
|
}
|
|
|
|
// memberExistsByID checks if a member exists by internal database ID
|
|
func (u *DefaultUser) memberExistsByID(ctx context.Context, id int64) (bool, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id"}, // Only select ID for existence check
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "id", Value: id},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return len(members) > 0, nil
|
|
}
|
|
|
|
// memberExistsByInvitationID checks if a member exists by invitation_id
|
|
func (u *DefaultUser) memberExistsByInvitationID(ctx context.Context, invitationID string) (bool, error) {
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id"}, // Only select ID for existence check
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "invitation_id", Value: invitationID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return false, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return len(members) > 0, nil
|
|
}
|
|
|
|
// CreateMember creates a new team member (user type)
|
|
func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAny) (string, error) {
|
|
// Validate required fields for user members
|
|
if _, exists := memberData["team_id"]; !exists {
|
|
return "", fmt.Errorf("team_id is required in memberData")
|
|
}
|
|
if _, exists := memberData["role_id"]; !exists {
|
|
return "", fmt.Errorf("role_id is required in memberData")
|
|
}
|
|
|
|
// Generate member_id if not provided
|
|
var generatedMemberID string
|
|
if _, exists := memberData["member_id"]; !exists || memberData["member_id"] == nil || memberData["member_id"] == "" {
|
|
memberID, err := u.generateMemberIDWithRetry(ctx)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate member ID: %w", err)
|
|
}
|
|
memberData["member_id"] = memberID
|
|
generatedMemberID = memberID
|
|
} else {
|
|
generatedMemberID = memberData["member_id"].(string)
|
|
}
|
|
|
|
// Add __yao_team_id to the member data
|
|
memberData["__yao_team_id"] = memberData["team_id"]
|
|
|
|
// Set default values if not provided
|
|
if _, exists := memberData["member_type"]; !exists {
|
|
memberData["member_type"] = "user"
|
|
}
|
|
if _, exists := memberData["status"]; !exists {
|
|
memberData["status"] = "pending"
|
|
}
|
|
|
|
// For user members, user_id is required unless it's an invitation (status=pending)
|
|
memberType := memberData["member_type"].(string)
|
|
status, _ := memberData["status"].(string)
|
|
|
|
if memberType == "user" && status != "pending" {
|
|
if _, exists := memberData["user_id"]; !exists {
|
|
return "", fmt.Errorf("user_id is required for active user members")
|
|
}
|
|
}
|
|
|
|
// Generate invitation_id for pending invitations
|
|
if status == "pending" && memberData["invitation_id"] == nil {
|
|
invitationID, err := u.generateInvitationID()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate invitation ID: %w", err)
|
|
}
|
|
memberData["invitation_id"] = invitationID
|
|
}
|
|
|
|
// Copy profile fields from user if not provided (for user members with user_id)
|
|
if memberType == "user" && memberData["user_id"] != nil && memberData["user_id"] != "" {
|
|
if userID, ok := memberData["user_id"].(string); ok {
|
|
u.copyMemberProfileFromUser(ctx, userID, memberData)
|
|
}
|
|
} else if memberType == "user" {
|
|
// If no user_id, still need to clean empty fields
|
|
for _, field := range []string{"display_name", "bio", "email"} {
|
|
if memberData[field] == nil || memberData[field] == "" {
|
|
delete(memberData, field)
|
|
}
|
|
}
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
_, err := m.Create(memberData)
|
|
if err != nil {
|
|
return "", fmt.Errorf(ErrFailedToCreateMember, err)
|
|
}
|
|
|
|
return generatedMemberID, nil
|
|
}
|
|
|
|
// CreateRobotMember creates a new robot member
|
|
func (u *DefaultUser) CreateRobotMember(ctx context.Context, teamID string, robotData maps.MapStrAny) (string, error) {
|
|
// Validate required fields for robot members
|
|
if _, exists := robotData["display_name"]; !exists {
|
|
return "", fmt.Errorf("display_name is required for robot members")
|
|
}
|
|
if _, exists := robotData["role_id"]; !exists {
|
|
return "", fmt.Errorf("role_id is required for robot members")
|
|
}
|
|
|
|
// Check if robot_email already exists globally (robot_email is globally unique)
|
|
if robotEmail, exists := robotData["robot_email"]; exists && robotEmail != nil && robotEmail != "" {
|
|
robotEmailStr := fmt.Sprintf("%v", robotEmail)
|
|
m := model.Select(u.memberModel)
|
|
existingMembers, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id"},
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "robot_email", Value: robotEmailStr},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to check robot_email uniqueness: %w", err)
|
|
}
|
|
if len(existingMembers) > 0 {
|
|
return "", fmt.Errorf("robot_email %s already exists", robotEmailStr)
|
|
}
|
|
}
|
|
|
|
memberData := maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"member_type": "robot",
|
|
"status": "active", // Robots are typically active by default
|
|
"user_id": nil, // Robots don't have user_id
|
|
}
|
|
|
|
// Copy shared profile fields (used by both users and robots)
|
|
profileFields := []string{
|
|
"display_name", "bio", "avatar", "email",
|
|
}
|
|
for _, field := range profileFields {
|
|
if value, exists := robotData[field]; exists {
|
|
memberData[field] = value
|
|
}
|
|
}
|
|
|
|
// Copy robot-specific fields
|
|
robotFields := []string{
|
|
"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",
|
|
"notes", "metadata",
|
|
"__yao_created_by", "__yao_updated_by", "__yao_team_id", "__yao_tenant_id",
|
|
}
|
|
|
|
for _, field := range robotFields {
|
|
if value, exists := robotData[field]; exists {
|
|
memberData[field] = value
|
|
}
|
|
}
|
|
|
|
// Set default robot status if not provided
|
|
if _, exists := memberData["robot_status"]; !exists {
|
|
memberData["robot_status"] = "idle"
|
|
}
|
|
|
|
// Set default autonomous mode if not provided
|
|
if _, exists := memberData["autonomous_mode"]; !exists {
|
|
memberData["autonomous_mode"] = false
|
|
}
|
|
|
|
return u.CreateMember(ctx, memberData)
|
|
}
|
|
|
|
// UpdateRobotMember updates a robot member by member_id
|
|
func (u *DefaultUser) UpdateRobotMember(ctx context.Context, memberID string, robotData maps.MapStrAny) error {
|
|
// First, verify the member exists and is a robot
|
|
existingMember, err := u.GetMemberByMemberID(ctx, memberID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get member: %w", err)
|
|
}
|
|
|
|
// Verify this is a robot member
|
|
memberType, exists := existingMember["member_type"]
|
|
if !exists || memberType != "robot" {
|
|
return fmt.Errorf("member %s is not a robot member", memberID)
|
|
}
|
|
|
|
// Check if robot_email already exists globally (if updating robot_email)
|
|
if robotEmail, exists := robotData["robot_email"]; exists && robotEmail != nil && robotEmail != "" {
|
|
robotEmailStr := fmt.Sprintf("%v", robotEmail)
|
|
|
|
// Only check uniqueness if the email is actually changing
|
|
currentEmail, _ := existingMember["robot_email"]
|
|
if currentEmail != robotEmailStr {
|
|
m := model.Select(u.memberModel)
|
|
existingMembers, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id", "member_id"},
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "robot_email", Value: robotEmailStr},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check robot_email uniqueness: %w", err)
|
|
}
|
|
if len(existingMembers) > 0 {
|
|
// Check if it's not the same member
|
|
existingMemberID, _ := existingMembers[0]["member_id"]
|
|
if existingMemberID != memberID {
|
|
return fmt.Errorf("robot_email %s already exists", robotEmailStr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
memberData := maps.MapStrAny{}
|
|
|
|
// Copy shared profile fields (used by both users and robots)
|
|
profileFields := []string{
|
|
"display_name", "bio", "avatar", "email",
|
|
}
|
|
for _, field := range profileFields {
|
|
if value, exists := robotData[field]; exists {
|
|
memberData[field] = value
|
|
}
|
|
}
|
|
|
|
// Copy robot-specific fields
|
|
robotFields := []string{
|
|
"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",
|
|
"notes", "metadata", "status",
|
|
"__yao_updated_by", "__yao_team_id", "__yao_tenant_id",
|
|
}
|
|
|
|
for _, field := range robotFields {
|
|
if value, exists := robotData[field]; exists {
|
|
memberData[field] = value
|
|
}
|
|
}
|
|
|
|
// Skip update if no valid fields to update
|
|
if len(memberData) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return u.UpdateMemberByMemberID(ctx, memberID, memberData)
|
|
}
|
|
|
|
// AddMember adds a user to a team (invitation-based)
|
|
func (u *DefaultUser) AddMember(ctx context.Context, teamID string, userID string, roleID string, invitedBy string) (string, error) {
|
|
// Check if member already exists
|
|
exists, err := u.MemberExists(ctx, teamID, userID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to check member existence: %w", err)
|
|
}
|
|
if exists {
|
|
return "", fmt.Errorf("user is already a member of this team")
|
|
}
|
|
|
|
// Generate invitation token
|
|
token, err := generateRandomPassword(32) // Use existing password generation for token
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate invitation token: %w", err)
|
|
}
|
|
|
|
memberData := maps.MapStrAny{
|
|
"team_id": teamID,
|
|
"user_id": userID,
|
|
"member_type": "user",
|
|
"role_id": roleID,
|
|
"status": "pending",
|
|
"invited_by": invitedBy,
|
|
"invited_at": time.Now(),
|
|
"invitation_token": token,
|
|
"invitation_expires_at": time.Now().Add(7 * 24 * time.Hour), // 7 days expiry
|
|
"__yao_created_by": invitedBy,
|
|
"__yao_team_id": teamID,
|
|
}
|
|
|
|
return u.CreateMember(ctx, memberData)
|
|
}
|
|
|
|
// AcceptInvitation accepts a team invitation
|
|
// userID can be empty - if provided and invitation doesn't have user_id, it will be updated
|
|
func (u *DefaultUser) AcceptInvitation(ctx context.Context, invitationID string, invitationToken string, userID string) error {
|
|
// Find member by invitation_id and token (including profile fields)
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(model.QueryParam{
|
|
Select: []interface{}{"id", "team_id", "user_id", "status", "invitation_expires_at", "display_name", "bio", "email"},
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "invitation_id", Value: invitationID},
|
|
{Column: "invitation_token", Value: invitationToken},
|
|
{Column: "status", Value: "pending"},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return fmt.Errorf("invitation not found or already accepted")
|
|
}
|
|
|
|
member := members[0]
|
|
|
|
// Check if invitation has expired
|
|
if expired, err := checkTimeExpired(member["invitation_expires_at"]); err == nil && expired {
|
|
return fmt.Errorf("invitation has expired")
|
|
}
|
|
|
|
// Update member status to active
|
|
memberID, err := parseIntFromDB(member["id"])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid member ID: %w", err)
|
|
}
|
|
updateData := maps.MapStrAny{
|
|
"status": "active",
|
|
"joined_at": time.Now(),
|
|
"invitation_token": nil, // Clear the token
|
|
"__yao_updated_by": userID, // Set the updated by user ID
|
|
"display_name": member["display_name"],
|
|
"bio": member["bio"],
|
|
"email": member["email"],
|
|
}
|
|
|
|
// If invitation doesn't have a user_id (unregistered user invitation), update it with provided userID
|
|
if (member["user_id"] == nil || member["user_id"] == "") && userID != "" {
|
|
updateData["user_id"] = userID
|
|
}
|
|
|
|
// Determine final user_id for profile copying
|
|
finalUserID := ""
|
|
if uid, ok := member["user_id"].(string); ok && uid != "" {
|
|
finalUserID = uid
|
|
} else if uid, ok := updateData["user_id"].(string); ok && uid != "" {
|
|
finalUserID = uid
|
|
}
|
|
|
|
// Copy profile fields from user if they are empty in updateData
|
|
// copyMemberProfileFromUser will also remove empty fields
|
|
u.copyMemberProfileFromUser(ctx, finalUserID, updateData)
|
|
|
|
affected, err := m.UpdateWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "id", Value: memberID},
|
|
},
|
|
Limit: 1,
|
|
}, updateData)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
return fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateMember updates an existing member
|
|
func (u *DefaultUser) UpdateMember(ctx context.Context, teamID string, userID string, memberData maps.MapStrAny) error {
|
|
// Remove sensitive fields that should not be updated directly
|
|
sensitiveFields := []string{"id", "member_id", "team_id", "user_id", "created_at", "invitation_token"}
|
|
for _, field := range sensitiveFields {
|
|
delete(memberData, field)
|
|
}
|
|
|
|
// Skip update if no valid fields remain
|
|
if len(memberData) == 0 {
|
|
return nil
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
affected, err := m.UpdateWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1,
|
|
}, memberData)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
// Check if member exists
|
|
exists, checkErr := u.MemberExists(ctx, teamID, userID)
|
|
if checkErr != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, checkErr)
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
// Member exists but no changes were made
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateMemberByID updates a member by internal database ID
|
|
func (u *DefaultUser) UpdateMemberByID(ctx context.Context, id int64, memberData maps.MapStrAny) error {
|
|
// Remove sensitive fields that should not be updated directly
|
|
sensitiveFields := []string{"id", "member_id", "team_id", "user_id", "created_at", "invitation_token"}
|
|
for _, field := range sensitiveFields {
|
|
delete(memberData, field)
|
|
}
|
|
|
|
// Skip update if no valid fields remain
|
|
if len(memberData) == 0 {
|
|
return nil
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
affected, err := m.UpdateWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "id", Value: id},
|
|
},
|
|
Limit: 1,
|
|
}, memberData)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
// Check if member exists
|
|
exists, checkErr := u.memberExistsByID(ctx, id)
|
|
if checkErr != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, checkErr)
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
// Member exists but no changes were made
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateMemberByMemberID updates a member by member_id (business ID)
|
|
func (u *DefaultUser) UpdateMemberByMemberID(ctx context.Context, memberID string, memberData maps.MapStrAny) error {
|
|
// Remove sensitive fields that should not be updated directly
|
|
sensitiveFields := []string{"id", "member_id", "team_id", "user_id", "created_at", "invitation_token"}
|
|
for _, field := range sensitiveFields {
|
|
delete(memberData, field)
|
|
}
|
|
|
|
// Skip update if no valid fields remain
|
|
if len(memberData) == 0 {
|
|
return nil
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
affected, err := m.UpdateWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "member_id", Value: memberID},
|
|
},
|
|
Limit: 1,
|
|
}, memberData)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, err)
|
|
}
|
|
|
|
// Note: affected=0 can mean either:
|
|
// 1. No record found with the given member_id
|
|
// 2. Record exists but no fields were changed (values are the same)
|
|
// We verify the member exists first to provide a more accurate error
|
|
if affected == 0 {
|
|
// Check if member exists
|
|
exists, checkErr := u.MemberExistsByMemberID(ctx, memberID)
|
|
if checkErr != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, checkErr)
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
// Member exists but no changes were made (values are the same)
|
|
// This is not an error, just return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveMember removes a member from a team (soft delete)
|
|
func (u *DefaultUser) RemoveMember(ctx context.Context, teamID string, userID string) error {
|
|
m := model.Select(u.memberModel)
|
|
affected, err := m.DeleteWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToDeleteMember, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
return fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveMemberByMemberID removes a member by member_id (business ID, soft delete)
|
|
func (u *DefaultUser) RemoveMemberByMemberID(ctx context.Context, memberID string) error {
|
|
m := model.Select(u.memberModel)
|
|
affected, err := m.DeleteWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "member_id", Value: memberID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToDeleteMember, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
return fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveAllTeamMembers removes all members from a team (used when deleting team)
|
|
func (u *DefaultUser) RemoveAllTeamMembers(ctx context.Context, teamID string) error {
|
|
m := model.Select(u.memberModel)
|
|
_, err := m.DeleteWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete all team members: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetTeamMembers retrieves all members of a team
|
|
func (u *DefaultUser) GetTeamMembers(ctx context.Context, teamID string) ([]maps.MapStr, error) {
|
|
param := model.QueryParam{
|
|
Select: u.memberFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
},
|
|
Orders: []model.QueryOrder{
|
|
{Column: "joined_at", Option: "desc"},
|
|
{Column: "invited_at", Option: "desc"},
|
|
},
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(param)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return members, nil
|
|
}
|
|
|
|
// GetUserTeams retrieves all teams a user is a member of
|
|
func (u *DefaultUser) GetUserTeams(ctx context.Context, userID string) ([]maps.MapStr, error) {
|
|
param := model.QueryParam{
|
|
Select: u.memberFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "user_id", Value: userID},
|
|
},
|
|
Orders: []model.QueryOrder{
|
|
{Column: "joined_at", Option: "desc"},
|
|
},
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(param)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return members, nil
|
|
}
|
|
|
|
// GetTeamMembersByStatus retrieves team members by status
|
|
func (u *DefaultUser) GetTeamMembersByStatus(ctx context.Context, teamID string, status string) ([]maps.MapStr, error) {
|
|
param := model.QueryParam{
|
|
Select: u.memberFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
{Column: "status", Value: status},
|
|
},
|
|
Orders: []model.QueryOrder{
|
|
{Column: "invited_at", Option: "desc"},
|
|
},
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(param)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return members, nil
|
|
}
|
|
|
|
// GetTeamRobotMembers retrieves all robot members of a team
|
|
func (u *DefaultUser) GetTeamRobotMembers(ctx context.Context, teamID string) ([]maps.MapStr, error) {
|
|
param := model.QueryParam{
|
|
Select: u.memberDetailFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
{Column: "member_type", Value: "robot"},
|
|
},
|
|
Orders: []model.QueryOrder{
|
|
{Column: "display_name", Option: "asc"},
|
|
},
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(param)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return members, nil
|
|
}
|
|
|
|
// GetActiveRobotMembers retrieves all active robot members across all teams
|
|
func (u *DefaultUser) GetActiveRobotMembers(ctx context.Context) ([]maps.MapStr, error) {
|
|
param := model.QueryParam{
|
|
Select: u.memberDetailFields,
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "member_type", Value: "robot"},
|
|
{Column: "autonomous_mode", Value: true},
|
|
{Column: "status", Value: "active"},
|
|
},
|
|
Orders: []model.QueryOrder{
|
|
{Column: "last_robot_activity", Option: "asc"}, // Oldest activity first
|
|
},
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
members, err := m.Get(param)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return members, nil
|
|
}
|
|
|
|
// UpdateMemberRole updates a member's role
|
|
func (u *DefaultUser) UpdateMemberRole(ctx context.Context, teamID string, userID string, roleID string) error {
|
|
updateData := maps.MapStrAny{
|
|
"role_id": roleID,
|
|
}
|
|
|
|
return u.UpdateMember(ctx, teamID, userID, updateData)
|
|
}
|
|
|
|
// UpdateMemberStatus updates a member's status
|
|
func (u *DefaultUser) UpdateMemberStatus(ctx context.Context, teamID string, userID string, status string) error {
|
|
updateData := maps.MapStrAny{
|
|
"status": status,
|
|
}
|
|
|
|
return u.UpdateMember(ctx, teamID, userID, updateData)
|
|
}
|
|
|
|
// UpdateMemberLastActivity updates a member's last activity time
|
|
func (u *DefaultUser) UpdateMemberLastActivity(ctx context.Context, teamID string, userID string) error {
|
|
updateData := maps.MapStrAny{
|
|
"last_active_at": time.Now(),
|
|
}
|
|
|
|
// Also increment login count
|
|
member, err := u.GetMember(ctx, teamID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
loginCount := int64(0)
|
|
if count := member["login_count"]; count != nil {
|
|
if parsedCount, err := parseIntFromDB(count); err == nil {
|
|
loginCount = parsedCount
|
|
}
|
|
}
|
|
updateData["login_count"] = loginCount + 1
|
|
|
|
return u.UpdateMember(ctx, teamID, userID, updateData)
|
|
}
|
|
|
|
// UpdateMemberRoleByMemberID updates a member's role by member_id
|
|
func (u *DefaultUser) UpdateMemberRoleByMemberID(ctx context.Context, memberID string, roleID string) error {
|
|
updateData := maps.MapStrAny{
|
|
"role_id": roleID,
|
|
}
|
|
|
|
return u.UpdateMemberByMemberID(ctx, memberID, updateData)
|
|
}
|
|
|
|
// UpdateMemberStatusByMemberID updates a member's status by member_id
|
|
func (u *DefaultUser) UpdateMemberStatusByMemberID(ctx context.Context, memberID string, status string) error {
|
|
updateData := maps.MapStrAny{
|
|
"status": status,
|
|
}
|
|
|
|
return u.UpdateMemberByMemberID(ctx, memberID, updateData)
|
|
}
|
|
|
|
// UpdateMemberLastActivityByMemberID updates a member's last activity time by member_id
|
|
func (u *DefaultUser) UpdateMemberLastActivityByMemberID(ctx context.Context, memberID string) error {
|
|
updateData := maps.MapStrAny{
|
|
"last_active_at": time.Now(),
|
|
}
|
|
|
|
// Also increment login count
|
|
member, err := u.GetMemberByMemberID(ctx, memberID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
loginCount := int64(0)
|
|
if count := member["login_count"]; count != nil {
|
|
if parsedCount, err := parseIntFromDB(count); err == nil {
|
|
loginCount = parsedCount
|
|
}
|
|
}
|
|
updateData["login_count"] = loginCount + 1
|
|
|
|
return u.UpdateMemberByMemberID(ctx, memberID, updateData)
|
|
}
|
|
|
|
// UpdateRobotActivity updates robot member's last activity and status by internal database ID
|
|
func (u *DefaultUser) UpdateRobotActivity(ctx context.Context, id int64, robotStatus string) error {
|
|
updateData := maps.MapStrAny{
|
|
"last_robot_activity": time.Now(),
|
|
"robot_status": robotStatus,
|
|
}
|
|
|
|
return u.UpdateMemberByID(ctx, id, updateData)
|
|
}
|
|
|
|
// UpdateMemberByInvitationID updates a member by invitation_id
|
|
func (u *DefaultUser) UpdateMemberByInvitationID(ctx context.Context, invitationID string, memberData maps.MapStrAny) error {
|
|
// Remove sensitive fields that should not be updated directly
|
|
// Note: user_id is allowed for invitation acceptance (pending -> active transition)
|
|
sensitiveFields := []string{"id", "team_id", "created_at", "invitation_id"}
|
|
for _, field := range sensitiveFields {
|
|
delete(memberData, field)
|
|
}
|
|
|
|
// Skip update if no valid fields remain
|
|
if len(memberData) == 0 {
|
|
return nil
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
affected, err := m.UpdateWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "invitation_id", Value: invitationID},
|
|
},
|
|
Limit: 1,
|
|
}, memberData)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
// Check if member exists
|
|
exists, checkErr := u.memberExistsByInvitationID(ctx, invitationID)
|
|
if checkErr != nil {
|
|
return fmt.Errorf(ErrFailedToUpdateMember, checkErr)
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
// Member exists but no changes were made
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveMemberByInvitationID removes a member by invitation_id
|
|
func (u *DefaultUser) RemoveMemberByInvitationID(ctx context.Context, invitationID string) error {
|
|
m := model.Select(u.memberModel)
|
|
affected, err := m.DeleteWhere(model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "invitation_id", Value: invitationID},
|
|
},
|
|
Limit: 1,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf(ErrFailedToDeleteMember, err)
|
|
}
|
|
|
|
if affected == 0 {
|
|
return fmt.Errorf(ErrMemberNotFound)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PaginateMembers retrieves paginated list of members
|
|
func (u *DefaultUser) PaginateMembers(ctx context.Context, param model.QueryParam, page int, pagesize int) (maps.MapStr, error) {
|
|
// Set default select fields if not provided
|
|
if param.Select == nil {
|
|
param.Select = u.memberFields
|
|
}
|
|
|
|
m := model.Select(u.memberModel)
|
|
result, err := m.Paginate(param, page, pagesize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(ErrFailedToGetMember, err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// copyMemberProfileFromUser copies member profile fields from user if not set in updateData
|
|
// Fields: display_name (from user.name), bio (n/a), avatar (from user.picture), email (from user.email)
|
|
// Only copies if the field is nil or empty in updateData
|
|
// Removes fields with nil or empty string values from updateData
|
|
func (u *DefaultUser) copyMemberProfileFromUser(ctx context.Context, userID string, updateData maps.MapStrAny) {
|
|
if userID == "" {
|
|
// Remove empty fields if no user_id
|
|
for _, field := range []string{"display_name", "bio", "avatar", "email"} {
|
|
if updateData[field] == nil || updateData[field] == "" {
|
|
delete(updateData, field)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check if we need to copy any fields
|
|
needsCopy := false
|
|
if updateData["display_name"] == nil || updateData["display_name"] == "" {
|
|
needsCopy = true
|
|
}
|
|
if updateData["avatar"] == nil || updateData["avatar"] == "" {
|
|
needsCopy = true
|
|
}
|
|
if updateData["email"] == nil || updateData["email"] == "" {
|
|
needsCopy = true
|
|
}
|
|
// bio field doesn't exist in user table, no need to check
|
|
|
|
if needsCopy {
|
|
// Get user profile using interface method
|
|
user, err := u.GetUser(ctx, userID)
|
|
if err == nil && user != nil {
|
|
// Copy display_name from user.name if not set
|
|
if (updateData["display_name"] == nil || updateData["display_name"] == "") && user["name"] != nil && user["name"] != "" {
|
|
updateData["display_name"] = user["name"]
|
|
}
|
|
|
|
// Copy avatar from user.picture if not set
|
|
if (updateData["avatar"] == nil || updateData["avatar"] == "") && user["picture"] != nil && user["picture"] != "" {
|
|
updateData["avatar"] = user["picture"]
|
|
}
|
|
|
|
// Copy email from user.email if not set
|
|
if (updateData["email"] == nil || updateData["email"] == "") && user["email"] != nil && user["email"] != "" {
|
|
updateData["email"] = user["email"]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove fields with nil or empty string values (should not be inserted to database)
|
|
for _, field := range []string{"display_name", "bio", "avatar", "email"} {
|
|
if updateData[field] == nil || updateData[field] == "" {
|
|
delete(updateData, field)
|
|
}
|
|
}
|
|
}
|