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:
Max 2025-10-23 19:40:29 +08:00
parent b9e71823ca
commit f31b3f5882
6 changed files with 311 additions and 166 deletions

File diff suppressed because one or more lines are too long

View file

@ -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",

View file

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

View file

@ -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

View file

@ -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 {

View file

@ -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": {