- Introduced a new `workspace` field across various robot-related structures, including `CreateRobotRequest`, `UpdateRobotRequest`, and `RobotResponse`, allowing for better organization and management of robots within specific workspaces. - Updated database queries and response mappings to accommodate the new workspace field, ensuring seamless integration with existing functionalities. - Enhanced agent execution context to include workspace information, improving the contextual awareness of agents during operations. - Added tests to validate the creation and updating of robots with workspace data, ensuring robust functionality and backward compatibility.
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", "workspace", "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", "workspace", "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)
|
|
}
|
|
}
|
|
}
|