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) } } }