Update member model to include profile fields and enhance invitation handling
- Added display_name, bio, and email fields to the member model for improved team-specific identity. - Updated team invitation creation and acceptance logic to handle new profile fields, ensuring proper data management and user experience. - Implemented logic to copy user profile fields when creating or updating members, enhancing data consistency.
This commit is contained in:
parent
b9e71823ca
commit
f31b3f5882
6 changed files with 311 additions and 166 deletions
284
data/bindata.go
284
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -157,14 +157,14 @@ var (
|
|||
|
||||
// DefaultMemberFields contains basic member fields
|
||||
DefaultMemberFields = []interface{}{
|
||||
"team_id", "user_id", "member_type", "role_id", "status",
|
||||
"team_id", "user_id", "member_type", "display_name", "bio", "email", "role_id", "status",
|
||||
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at",
|
||||
"last_active_at", "login_count", "message", "created_at", "updated_at",
|
||||
"last_active_at", "login_count", "created_at", "updated_at",
|
||||
}
|
||||
|
||||
// DefaultMemberDetailFields contains all member fields including robot config and permissions
|
||||
DefaultMemberDetailFields = []interface{}{
|
||||
"team_id", "user_id", "member_type", "role_id", "status",
|
||||
"team_id", "user_id", "member_type", "display_name", "bio", "email", "role_id", "status",
|
||||
"robot_name", "robot_description", "robot_avatar", "robot_config", "agents", "tools",
|
||||
"mcp_servers", "data_access_permissions", "system_prompt", "is_active_robot",
|
||||
"schedule_config", "random_activity", "activity_frequency", "last_robot_activity",
|
||||
|
|
|
|||
|
|
@ -160,6 +160,20 @@ func (u *DefaultUser) CreateMember(ctx context.Context, memberData maps.MapStrAn
|
|||
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)
|
||||
id, err := m.Create(memberData)
|
||||
if err != nil {
|
||||
|
|
@ -243,10 +257,10 @@ func (u *DefaultUser) AddMember(ctx context.Context, teamID string, userID strin
|
|||
// 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
|
||||
// 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"},
|
||||
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},
|
||||
|
|
@ -280,13 +294,28 @@ func (u *DefaultUser) AcceptInvitation(ctx context.Context, invitationID string,
|
|||
"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 userID != "" && (member["user_id"] == nil || member["user_id"] == "") {
|
||||
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},
|
||||
|
|
@ -638,3 +667,52 @@ func (u *DefaultUser) PaginateMembers(ctx context.Context, param model.QueryPara
|
|||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// copyMemberProfileFromUser copies member profile fields from user if not set in updateData
|
||||
// Fields: display_name (from user.name), bio (n/a), 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", "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["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 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", "email"} {
|
||||
if updateData[field] == nil || updateData[field] == "" {
|
||||
delete(updateData, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ func GinTeamUpdate(c *gin.Context) {
|
|||
|
||||
// GinTeamCurrent handles GET /teams/current - Get current team
|
||||
func GinTeamCurrent(c *gin.Context) {
|
||||
|
||||
// Get authorized user info
|
||||
authInfo := authorized.GetInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
|
|
@ -325,23 +325,13 @@ func GinTeamCurrent(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Get current team
|
||||
teamID := authInfo.TeamID
|
||||
// Get current team ID (from token or first owner team)
|
||||
teamID, err := getCurrentTeamID(c.Request.Context(), authInfo.TeamID, authInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get current team ID: %v", err)
|
||||
|
||||
// If no team ID, get the user teams from provider first
|
||||
if teamID == "" {
|
||||
// Get user teams
|
||||
teams, err := getOwnerTeams(c.Request.Context(), authInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get owner teams: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to get owner teams",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
}
|
||||
|
||||
if len(teams) == 0 {
|
||||
// Return 404 if user has no team
|
||||
if err.Error() == "no owner team found for user" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "No owner team found for user",
|
||||
|
|
@ -350,7 +340,13 @@ func GinTeamCurrent(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
teamID = teams[0]["team_id"].(string)
|
||||
// Return 500 for other errors
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to get current team ID",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get team details
|
||||
|
|
@ -362,6 +358,7 @@ func GinTeamCurrent(c *gin.Context) {
|
|||
ErrorDescription: "Failed to get team details",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, team)
|
||||
|
|
@ -713,6 +710,34 @@ func teamList(ctx context.Context, userID string, param model.QueryParam, page,
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// getCurrentTeamID resolves the current team ID for a user
|
||||
// If teamID is provided, it returns it directly
|
||||
// If teamID is empty, it gets the first owner team for the user
|
||||
func getCurrentTeamID(ctx context.Context, teamID, userID string) (string, error) {
|
||||
// If team ID is already provided, return it
|
||||
if teamID != "" {
|
||||
return teamID, nil
|
||||
}
|
||||
|
||||
// Get owner teams for the user
|
||||
teams, err := getOwnerTeams(ctx, userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get owner teams: %w", err)
|
||||
}
|
||||
|
||||
// Check if user has any owner teams
|
||||
if len(teams) == 0 {
|
||||
return "", fmt.Errorf("no owner team found for user")
|
||||
}
|
||||
|
||||
// Return the first team ID
|
||||
if teamIDVal, ok := teams[0]["team_id"].(string); ok {
|
||||
return teamIDVal, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid team_id format")
|
||||
}
|
||||
|
||||
// teamGet handles the business logic for getting a specific user team
|
||||
func teamGet(ctx context.Context, userID, teamID string) (maps.MapStrAny, error) {
|
||||
// Get user provider instance
|
||||
|
|
|
|||
|
|
@ -1040,6 +1040,13 @@ func teamInvitationGet(ctx context.Context, userID, teamID, invitationID string)
|
|||
// 1. Email invitation: provide email and role, send invitation link via email
|
||||
// 2. Link invitation: create invitation link for display in frontend, customizable expiry
|
||||
func teamInvitationCreate(ctx context.Context, userID, teamID string, invitationData maps.MapStrAny) (string, error) {
|
||||
// Remove empty string fields (should not be inserted to database)
|
||||
for _, field := range []string{"user_id", "email", "message", "display_name", "bio"} {
|
||||
if invitationData[field] == "" {
|
||||
delete(invitationData, field)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user has access to the team (write permission: owner only)
|
||||
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,35 @@
|
|||
"nullable": false
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Member Profile Fields (Team-specific identity)
|
||||
// ============================================================================
|
||||
{
|
||||
"name": "display_name",
|
||||
"type": "string",
|
||||
"label": "Display Name",
|
||||
"comment": "Display name for this member within the team (can differ from user.name)",
|
||||
"length": 200,
|
||||
"nullable": true,
|
||||
"index": true
|
||||
},
|
||||
{
|
||||
"name": "bio",
|
||||
"type": "text",
|
||||
"label": "Bio",
|
||||
"comment": "Personal bio/description for this member within the team",
|
||||
"nullable": true
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"label": "Email",
|
||||
"comment": "Email for this member within the team (can differ from user.email)",
|
||||
"length": 255,
|
||||
"nullable": true,
|
||||
"index": true
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// Role & Permission Fields
|
||||
// ============================================================================
|
||||
|
|
@ -395,6 +424,12 @@
|
|||
"columns": ["team_id", "last_active_at"],
|
||||
"type": "index",
|
||||
"comment": "Index for team activity tracking"
|
||||
},
|
||||
{
|
||||
"name": "idx_team_display_name",
|
||||
"columns": ["team_id", "display_name"],
|
||||
"type": "index",
|
||||
"comment": "Index for searching members by display name within team"
|
||||
}
|
||||
],
|
||||
"relations": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue