- 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.
1690 lines
52 KiB
Go
1690 lines
52 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yaoapp/gou/model"
|
|
"github.com/yaoapp/gou/process"
|
|
"github.com/yaoapp/kun/exception"
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/kun/maps"
|
|
"github.com/yaoapp/yao/openapi/oauth"
|
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
|
"github.com/yaoapp/yao/openapi/response"
|
|
"github.com/yaoapp/yao/openapi/utils"
|
|
)
|
|
|
|
// Member Management Handlers
|
|
|
|
// GinMemberList handles GET /teams/:team_id/members - Get team members with advanced filtering
|
|
func GinMemberList(c *gin.Context) {
|
|
authInfo := authorized.GetInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
if teamID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Parse request parameters
|
|
var req MemberListRequest
|
|
if err := c.ShouldBindQuery(&req); err != nil {
|
|
// Provide a more user-friendly error message
|
|
errMsg := "Invalid query parameters"
|
|
if strings.Contains(err.Error(), "parsing") {
|
|
errMsg = "Invalid query parameter format. Please check page, pagesize, and other numeric values."
|
|
}
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: errMsg,
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Set default values
|
|
if req.Page <= 0 {
|
|
req.Page = 1
|
|
}
|
|
if req.PageSize <= 0 {
|
|
req.PageSize = 20
|
|
}
|
|
if req.PageSize > 100 {
|
|
req.PageSize = 100
|
|
}
|
|
if req.Order == "" {
|
|
req.Order = "created_at desc"
|
|
}
|
|
|
|
// Parse fields from comma-separated string if provided
|
|
if fieldsStr := c.Query("fields"); fieldsStr != "" {
|
|
req.Fields = strings.Split(fieldsStr, ",")
|
|
// Trim spaces from field names
|
|
for i, field := range req.Fields {
|
|
req.Fields[i] = strings.TrimSpace(field)
|
|
}
|
|
}
|
|
|
|
// Get request base URL for invitation link generation
|
|
requestBaseURL := getRequestBaseURL(c)
|
|
|
|
// Get locale from query parameter or default to "en"
|
|
locale := c.Query("locale")
|
|
if locale == "" {
|
|
locale = "en"
|
|
}
|
|
|
|
// Call business logic
|
|
result, err := memberList(c.Request.Context(), authInfo.UserID, teamID, &req, requestBaseURL, locale)
|
|
if err != nil {
|
|
log.Error("Failed to get team members: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else if strings.Contains(err.Error(), "invalid") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to retrieve team members",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Return the paginated result
|
|
response.RespondWithSuccess(c, http.StatusOK, result)
|
|
}
|
|
|
|
// GinMemberCheckRobotEmail handles GET /api/user/teams/:id/members/check-robot-email?robot_email=xxx - Check if robot email exists globally
|
|
func GinMemberCheckRobotEmail(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := oauth.GetAuthorizedInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
if teamID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
robotEmail := c.Query("robot_email")
|
|
if robotEmail == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "robot_email query parameter is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Call business logic
|
|
exists, err := memberCheckRobotEmail(c.Request.Context(), authInfo.UserID, teamID, robotEmail)
|
|
if err != nil {
|
|
log.Error("Failed to check robot email: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: fmt.Sprintf("Failed to check robot email: %v", err),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Return result
|
|
result := map[string]interface{}{
|
|
"exists": exists,
|
|
"robot_email": robotEmail,
|
|
}
|
|
response.RespondWithSuccess(c, http.StatusOK, result)
|
|
}
|
|
|
|
// GinMemberGet handles GET /teams/:team_id/members/:member_id - Get team member details
|
|
func GinMemberGet(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := oauth.GetAuthorizedInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
memberID := c.Param("member_id")
|
|
if teamID == "" || memberID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID and Member ID are required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Call business logic
|
|
memberData, err := memberGet(c.Request.Context(), authInfo.UserID, teamID, memberID)
|
|
if err != nil {
|
|
log.Error("Failed to get member details: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Member not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to retrieve member details",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Convert to response format
|
|
member := mapToMemberDetailResponse(memberData)
|
|
response.RespondWithSuccess(c, http.StatusOK, member)
|
|
}
|
|
|
|
// GinMemberCreateRobot handles POST /teams/:team_id/members/robots - Add robot member to team
|
|
func GinMemberCreateRobot(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := authorized.GetInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
if teamID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var req CreateRobotMemberRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Prepare base robot member data
|
|
baseData := maps.MapStrAny{
|
|
"display_name": req.Name,
|
|
"robot_email": req.RobotEmail, // Required: globally unique email
|
|
"bio": req.Bio,
|
|
"role_id": req.RoleID,
|
|
"system_prompt": req.SystemPrompt,
|
|
"autonomous_mode": utils.ToBool(req.AutonomousMode),
|
|
}
|
|
|
|
// Add optional fields
|
|
if req.Avatar != "" {
|
|
baseData["avatar"] = req.Avatar
|
|
}
|
|
if req.Email != "" {
|
|
baseData["email"] = req.Email // Optional: display-only email
|
|
}
|
|
if len(req.AuthorizedSenders) > 0 {
|
|
baseData["authorized_senders"] = req.AuthorizedSenders
|
|
}
|
|
if len(req.EmailFilterRules) > 0 {
|
|
baseData["email_filter_rules"] = req.EmailFilterRules
|
|
}
|
|
if req.ManagerID != "" {
|
|
baseData["manager_id"] = req.ManagerID
|
|
}
|
|
if req.LanguageModel != "" {
|
|
baseData["language_model"] = req.LanguageModel
|
|
}
|
|
if req.Workspace != "" {
|
|
baseData["workspace"] = req.Workspace
|
|
}
|
|
if len(req.Agents) > 0 {
|
|
baseData["agents"] = req.Agents
|
|
}
|
|
if len(req.MCPServers) > 0 {
|
|
baseData["mcp_servers"] = req.MCPServers
|
|
}
|
|
if req.CostLimit > 0 {
|
|
baseData["cost_limit"] = req.CostLimit
|
|
}
|
|
|
|
// Wrap with create scope for permission tracking
|
|
robotData := authInfo.WithCreateScope(baseData)
|
|
|
|
// Call business logic
|
|
memberID, err := memberCreateRobot(c.Request.Context(), authInfo.UserID, teamID, robotData)
|
|
if err != nil {
|
|
log.Error("Failed to create robot member: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusConflict, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to create robot member",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Return created member ID
|
|
response.RespondWithSuccess(c, http.StatusCreated, gin.H{"member_id": memberID})
|
|
}
|
|
|
|
// GinMemberUpdateRobot handles PUT /teams/:team_id/members/robots/:member_id - Update robot member
|
|
func GinMemberUpdateRobot(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := authorized.GetInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
memberID := c.Param("member_id")
|
|
if teamID == "" || memberID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID and Member ID are required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var req UpdateRobotMemberRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Prepare update data
|
|
updateData := maps.MapStrAny{}
|
|
|
|
// Add fields if provided
|
|
if req.Name != "" {
|
|
updateData["display_name"] = req.Name
|
|
}
|
|
if req.Avatar != "" {
|
|
updateData["avatar"] = req.Avatar
|
|
}
|
|
if req.Email != "" {
|
|
updateData["email"] = req.Email
|
|
}
|
|
if req.RobotEmail != "" {
|
|
updateData["robot_email"] = req.RobotEmail
|
|
}
|
|
if req.Bio != "" {
|
|
updateData["bio"] = req.Bio
|
|
}
|
|
if req.RoleID != "" {
|
|
updateData["role_id"] = req.RoleID
|
|
}
|
|
if req.ManagerID != "" {
|
|
updateData["manager_id"] = req.ManagerID
|
|
}
|
|
if req.SystemPrompt != "" {
|
|
updateData["system_prompt"] = req.SystemPrompt
|
|
}
|
|
if req.LanguageModel != "" {
|
|
updateData["language_model"] = req.LanguageModel
|
|
}
|
|
if req.Workspace != nil {
|
|
updateData["workspace"] = *req.Workspace
|
|
}
|
|
if req.Status != "" {
|
|
updateData["status"] = req.Status
|
|
}
|
|
if req.RobotStatus != "" {
|
|
updateData["robot_status"] = req.RobotStatus
|
|
}
|
|
if req.AutonomousMode != "" {
|
|
updateData["autonomous_mode"] = utils.ToBool(req.AutonomousMode)
|
|
}
|
|
if req.CostLimit > 0 {
|
|
updateData["cost_limit"] = req.CostLimit
|
|
}
|
|
|
|
// Handle array fields (they can be empty arrays)
|
|
if req.AuthorizedSenders != nil {
|
|
updateData["authorized_senders"] = req.AuthorizedSenders
|
|
}
|
|
if req.EmailFilterRules != nil {
|
|
updateData["email_filter_rules"] = req.EmailFilterRules
|
|
}
|
|
if req.Agents != nil {
|
|
updateData["agents"] = req.Agents
|
|
}
|
|
if req.MCPServers != nil {
|
|
updateData["mcp_servers"] = req.MCPServers
|
|
}
|
|
|
|
// Wrap with update scope for permission tracking
|
|
robotData := authInfo.WithUpdateScope(updateData)
|
|
|
|
// Call business logic
|
|
err := memberUpdateRobot(c.Request.Context(), authInfo.UserID, teamID, memberID, robotData)
|
|
if err != nil {
|
|
log.Error("Failed to update robot member: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else if strings.Contains(err.Error(), "not a robot member") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
} else if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusConflict, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to update robot member",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Return success
|
|
response.RespondWithSuccess(c, http.StatusOK, gin.H{"message": "Robot member updated successfully"})
|
|
}
|
|
|
|
// GinMemberUpdate handles PUT /teams/:team_id/members/:member_id - Update team member
|
|
func GinMemberUpdate(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := oauth.GetAuthorizedInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
memberID := c.Param("member_id")
|
|
if teamID == "" || memberID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID and Member ID are required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var req UpdateMemberRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Prepare update data
|
|
updateData := maps.MapStrAny{}
|
|
|
|
if req.RoleID != "" {
|
|
updateData["role_id"] = req.RoleID
|
|
}
|
|
if req.Status != "" {
|
|
updateData["status"] = req.Status
|
|
}
|
|
if req.Settings != nil {
|
|
updateData["settings"] = req.Settings
|
|
}
|
|
if req.LastActivity != "" {
|
|
updateData["last_activity"] = req.LastActivity
|
|
}
|
|
|
|
// Call business logic
|
|
err := memberUpdate(c.Request.Context(), authInfo.UserID, teamID, memberID, updateData)
|
|
if err != nil {
|
|
log.Error("Failed to update member: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Member not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to update member",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
response.RespondWithSuccess(c, http.StatusOK, gin.H{"message": "Member updated successfully"})
|
|
}
|
|
|
|
// GinMemberGetProfile handles GET /teams/:team_id/members/:member_id/profile - Get member profile
|
|
// Note: :member_id in the route actually contains user_id for profile retrieval
|
|
func GinMemberGetProfile(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := authorized.GetInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
// For profile retrieval, :member_id parameter contains the user_id (not member_id)
|
|
memberUserID := c.Param("member_id")
|
|
if teamID == "" || memberUserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID and User ID are required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Call business logic
|
|
profile, err := memberGetProfile(c.Request.Context(), authInfo.UserID, teamID, memberUserID)
|
|
if err != nil {
|
|
log.Error("Failed to get member profile: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Member not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to get member profile",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
response.RespondWithSuccess(c, http.StatusOK, profile)
|
|
}
|
|
|
|
// GinMemberUpdateProfile handles PUT /teams/:team_id/members/:member_id/profile - Update member profile
|
|
// Note: :member_id in the route actually contains user_id for profile updates
|
|
func GinMemberUpdateProfile(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := authorized.GetInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
// For profile updates, :member_id parameter contains the user_id (not member_id)
|
|
memberUserID := c.Param("member_id")
|
|
if teamID == "" || memberUserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID and User ID are required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Parse request body
|
|
var req UpdateMemberProfileRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Invalid request body: " + err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Call business logic
|
|
err := memberUpdateProfile(c.Request.Context(), authInfo.UserID, teamID, memberUserID, req)
|
|
if err != nil {
|
|
log.Error("Failed to update member profile: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Member not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else if strings.Contains(err.Error(), "no fields to update") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to update member profile",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
response.RespondWithSuccess(c, http.StatusOK, gin.H{
|
|
"user_id": memberUserID,
|
|
"message": "Member profile updated successfully",
|
|
})
|
|
}
|
|
|
|
// GinMemberDelete handles DELETE /teams/:team_id/members/:member_id - Remove team member
|
|
func GinMemberDelete(c *gin.Context) {
|
|
// Get authorized user info
|
|
authInfo := oauth.GetAuthorizedInfo(c)
|
|
if authInfo == nil || authInfo.UserID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidClient.Code,
|
|
ErrorDescription: "User not authenticated",
|
|
}
|
|
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
|
return
|
|
}
|
|
|
|
teamID := c.Param("id")
|
|
memberID := c.Param("member_id")
|
|
if teamID == "" || memberID == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Team ID and Member ID are required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Call business logic
|
|
err := memberDelete(c.Request.Context(), authInfo.UserID, teamID, memberID)
|
|
if err != nil {
|
|
log.Error("Failed to delete member: %v", err)
|
|
// Check error type for appropriate response
|
|
if strings.Contains(err.Error(), "not found") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Member not found",
|
|
}
|
|
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
|
} else if strings.Contains(err.Error(), "access denied") {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrAccessDenied.Code,
|
|
ErrorDescription: err.Error(),
|
|
}
|
|
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
|
} else {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrServerError.Code,
|
|
ErrorDescription: "Failed to delete member",
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
response.RespondWithSuccess(c, http.StatusOK, gin.H{"message": "Member removed successfully"})
|
|
}
|
|
|
|
// Yao Process Handlers (for Yao application calls)
|
|
|
|
// ProcessMemberList user.member.list Member list processor
|
|
// Args[0] string: team_id
|
|
// Args[1] map: Query parameters with advanced filtering
|
|
//
|
|
// {
|
|
// "page": 1, "pagesize": 20,
|
|
// "status": "active", "member_type": "user", "role_id": "admin",
|
|
// "email": "test@example.com", "display_name": "John",
|
|
// "order": "created_at desc",
|
|
// "fields": ["id", "user_id", "display_name", "role_id"]
|
|
// }
|
|
//
|
|
// Return: map: Paginated member list
|
|
func ProcessMemberList(process *process.Process) interface{} {
|
|
process.ValidateArgNums(2)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
if teamID == "" {
|
|
exception.New("team_id is required", 400).Throw()
|
|
}
|
|
|
|
// Parse query parameters
|
|
queryMap := process.ArgsMap(1)
|
|
|
|
// Build request object
|
|
req := &MemberListRequest{
|
|
Page: 1,
|
|
PageSize: 20,
|
|
Order: "created_at desc",
|
|
}
|
|
|
|
// Parse pagination
|
|
if p, ok := queryMap["page"]; ok {
|
|
if pageInt, ok := p.(int); ok && pageInt > 0 {
|
|
req.Page = pageInt
|
|
}
|
|
}
|
|
|
|
if ps, ok := queryMap["pagesize"]; ok {
|
|
if pagesizeInt, ok := ps.(int); ok && pagesizeInt > 0 && pagesizeInt <= 100 {
|
|
req.PageSize = pagesizeInt
|
|
}
|
|
}
|
|
|
|
// Parse filters
|
|
if status, ok := queryMap["status"].(string); ok {
|
|
req.Status = status
|
|
}
|
|
|
|
if memberType, ok := queryMap["member_type"].(string); ok {
|
|
req.MemberType = memberType
|
|
}
|
|
|
|
if roleID, ok := queryMap["role_id"].(string); ok {
|
|
req.RoleID = roleID
|
|
}
|
|
|
|
if email, ok := queryMap["email"].(string); ok {
|
|
req.Email = email
|
|
}
|
|
|
|
if displayName, ok := queryMap["display_name"].(string); ok {
|
|
req.DisplayName = displayName
|
|
}
|
|
|
|
// Parse sorting
|
|
if order, ok := queryMap["order"].(string); ok {
|
|
req.Order = order
|
|
}
|
|
|
|
// Parse fields selection
|
|
if fields, ok := queryMap["fields"]; ok {
|
|
if fieldsSlice, ok := fields.([]interface{}); ok {
|
|
req.Fields = make([]string, 0, len(fieldsSlice))
|
|
for _, f := range fieldsSlice {
|
|
if fieldStr, ok := f.(string); ok {
|
|
req.Fields = append(req.Fields, fieldStr)
|
|
}
|
|
}
|
|
} else if fieldsStrSlice, ok := fields.([]string); ok {
|
|
req.Fields = fieldsStrSlice
|
|
}
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Get locale from query map if available, default to "en"
|
|
locale := "en"
|
|
if localeVal, ok := queryMap["locale"].(string); ok && localeVal != "" {
|
|
locale = localeVal
|
|
}
|
|
|
|
// Call business logic (no requestBaseURL available in process context, use empty string)
|
|
result, err := memberList(ctx, userIDStr, teamID, req, "", locale)
|
|
if err != nil {
|
|
exception.New("failed to list members: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ProcessMemberGet user.member.get Member get processor
|
|
// Args[0] string: team_id
|
|
// Args[1] string: member_id
|
|
// Return: map: Member details
|
|
func ProcessMemberGet(process *process.Process) interface{} {
|
|
process.ValidateArgNums(2)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
memberID := process.ArgsString(1)
|
|
|
|
if teamID == "" || memberID == "" {
|
|
exception.New("team_id and member_id are required", 400).Throw()
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
result, err := memberGet(ctx, userIDStr, teamID, memberID)
|
|
if err != nil {
|
|
exception.New("failed to get member: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ProcessMemberUpdate user.member.update Member update processor
|
|
// Args[0] string: team_id
|
|
// Args[1] string: member_id
|
|
// Args[2] map: Update data {"role_id": "admin", "status": "active", "settings": {...}}
|
|
// Return: map: {"message": "success"}
|
|
func ProcessMemberUpdate(process *process.Process) interface{} {
|
|
process.ValidateArgNums(3)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
memberID := process.ArgsString(1)
|
|
updateData := maps.MapStrAny(process.ArgsMap(2))
|
|
|
|
if teamID == "" || memberID == "" {
|
|
exception.New("team_id and member_id are required", 400).Throw()
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
err := memberUpdate(ctx, userIDStr, teamID, memberID, updateData)
|
|
if err != nil {
|
|
exception.New("failed to update member: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"message": "success",
|
|
}
|
|
}
|
|
|
|
// ProcessMemberGetProfile user.member.profile.get Member profile get processor
|
|
// Args[0] string: team_id
|
|
// Args[1] string: user_id (not member_id)
|
|
// Return: map: Member profile data
|
|
func ProcessMemberGetProfile(process *process.Process) interface{} {
|
|
process.ValidateArgNums(2)
|
|
|
|
// Get user_id from session
|
|
requestUserID := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
memberUserID := process.ArgsString(1)
|
|
|
|
if teamID == "" || memberUserID == "" {
|
|
exception.New("team_id and user_id are required", 400).Throw()
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
result, err := memberGetProfile(ctx, requestUserID, teamID, memberUserID)
|
|
if err != nil {
|
|
exception.New("failed to get member profile: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ProcessMemberUpdateProfile user.member.profile.update Member profile update processor
|
|
// Args[0] string: team_id
|
|
// Args[1] string: user_id (member's user_id)
|
|
// Args[2] map: Update profile data {"display_name": "John Doe", "bio": "Developer", "avatar": "https://...", "email": "john@example.com"}
|
|
// Return: map: {"user_id": "xxx", "message": "success"}
|
|
func ProcessMemberUpdateProfile(process *process.Process) interface{} {
|
|
process.ValidateArgNums(3)
|
|
|
|
// Get user_id from session
|
|
requestUserID := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
memberUserID := process.ArgsString(1)
|
|
profileData := process.ArgsMap(2)
|
|
|
|
if teamID == "" || memberUserID == "" {
|
|
exception.New("team_id and user_id are required", 400).Throw()
|
|
}
|
|
|
|
// Build UpdateMemberProfileRequest from map
|
|
req := UpdateMemberProfileRequest{}
|
|
if displayName, ok := profileData["display_name"].(string); ok && displayName != "" {
|
|
req.DisplayName = &displayName
|
|
}
|
|
if bio, ok := profileData["bio"].(string); ok && bio != "" {
|
|
req.Bio = &bio
|
|
}
|
|
if avatar, ok := profileData["avatar"].(string); ok && avatar != "" {
|
|
req.Avatar = &avatar
|
|
}
|
|
if email, ok := profileData["email"].(string); ok && email != "" {
|
|
req.Email = &email
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
err := memberUpdateProfile(ctx, requestUserID, teamID, memberUserID, req)
|
|
if err != nil {
|
|
exception.New("failed to update member profile: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"user_id": memberUserID,
|
|
"message": "success",
|
|
}
|
|
}
|
|
|
|
// ProcessMemberDelete user.member.delete Member delete processor
|
|
// Args[0] string: team_id
|
|
// Args[1] string: member_id
|
|
// Return: map: {"message": "success"}
|
|
func ProcessMemberDelete(process *process.Process) interface{} {
|
|
process.ValidateArgNums(2)
|
|
|
|
// Get user_id from session
|
|
userIDStr := GetUserIDFromSession(process)
|
|
|
|
teamID := process.ArgsString(0)
|
|
memberID := process.ArgsString(1)
|
|
|
|
if teamID == "" || memberID == "" {
|
|
exception.New("team_id and member_id are required", 400).Throw()
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
err := memberDelete(ctx, userIDStr, teamID, memberID)
|
|
if err != nil {
|
|
exception.New("failed to delete member: %s", 500, err.Error()).Throw()
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"message": "success",
|
|
}
|
|
}
|
|
|
|
// Private Business Logic Functions (internal use only)
|
|
|
|
// memberList handles the business logic for listing team members with advanced filtering
|
|
func memberList(ctx context.Context, userID, teamID string, req *MemberListRequest, requestBaseURL, locale string) (maps.MapStr, error) {
|
|
// Check if user has access to the team (read permission: owner or member)
|
|
isOwner, isMember, err := checkTeamAccess(ctx, teamID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Allow access if user is owner or member
|
|
if !isOwner && !isMember {
|
|
return nil, fmt.Errorf("access denied: user is not a member of this team")
|
|
}
|
|
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Get team configuration for invitation link generation
|
|
teamConfig := GetTeamConfig(locale)
|
|
|
|
// Build query parameters
|
|
param := model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
},
|
|
}
|
|
|
|
// Add filters
|
|
if req.Status != "" {
|
|
// Validate status values
|
|
validStatuses := map[string]bool{
|
|
"pending": true, "active": true, "inactive": true, "suspended": true,
|
|
}
|
|
if !validStatuses[req.Status] {
|
|
return nil, fmt.Errorf("invalid status value: %s (must be one of: pending, active, inactive, suspended)", req.Status)
|
|
}
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "status",
|
|
Value: req.Status,
|
|
})
|
|
}
|
|
|
|
if req.MemberType != "" {
|
|
// Validate member type values
|
|
validTypes := map[string]bool{
|
|
"user": true, "robot": true,
|
|
}
|
|
if !validTypes[req.MemberType] {
|
|
return nil, fmt.Errorf("invalid member_type value: %s (must be one of: user, robot)", req.MemberType)
|
|
}
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "member_type",
|
|
Value: req.MemberType,
|
|
})
|
|
}
|
|
|
|
if req.RoleID != "" {
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "role_id",
|
|
Value: req.RoleID,
|
|
})
|
|
}
|
|
|
|
if req.Email != "" {
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "email",
|
|
Value: req.Email,
|
|
})
|
|
}
|
|
|
|
if req.DisplayName != "" {
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "display_name",
|
|
Value: req.DisplayName,
|
|
OP: "like",
|
|
})
|
|
}
|
|
|
|
// Parse and validate sorting
|
|
validOrderFields := map[string]bool{
|
|
"created_at": true,
|
|
"joined_at": true,
|
|
}
|
|
validOrderDirs := map[string]bool{
|
|
"asc": true, "desc": true,
|
|
}
|
|
|
|
// Parse order field (format: "field_name [asc|desc]")
|
|
orderParts := strings.Fields(req.Order) // Split by whitespace
|
|
orderBy := ""
|
|
orderDir := "desc" // Default direction
|
|
|
|
if len(orderParts) > 0 {
|
|
orderBy = orderParts[0]
|
|
if len(orderParts) > 1 {
|
|
orderDir = strings.ToLower(orderParts[1])
|
|
}
|
|
}
|
|
|
|
// Build sorting with priority: owner first, then pending invitations, then others
|
|
orders := []model.QueryOrder{
|
|
{Column: "is_owner", Option: "desc"}, // Owners always first
|
|
{Column: "status", Option: "asc"}, // Then pending before active (enum index: pending=1 < active=2 < inactive=3 < suspended=4)
|
|
}
|
|
|
|
// Validate and add user-specified order field
|
|
if orderBy != "" {
|
|
if !validOrderFields[orderBy] {
|
|
return nil, fmt.Errorf("invalid order field: %s (must be one of: created_at, joined_at)", orderBy)
|
|
}
|
|
if !validOrderDirs[orderDir] {
|
|
return nil, fmt.Errorf("invalid order direction: %s (must be one of: asc, desc)", orderDir)
|
|
}
|
|
orders = append(orders, model.QueryOrder{
|
|
Column: orderBy, Option: orderDir,
|
|
})
|
|
} else {
|
|
// Default tertiary sorting
|
|
orders = append(orders, model.QueryOrder{
|
|
Column: "created_at", Option: "desc",
|
|
})
|
|
}
|
|
|
|
param.Orders = orders
|
|
|
|
// Add field selection if specified
|
|
if len(req.Fields) > 0 {
|
|
// Convert []string to []interface{} for QueryParam.Select
|
|
param.Select = make([]interface{}, len(req.Fields))
|
|
for i, field := range req.Fields {
|
|
param.Select[i] = field
|
|
}
|
|
}
|
|
|
|
// Get paginated members
|
|
result, err := provider.PaginateMembers(ctx, param, req.Page, req.PageSize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve members: %w", err)
|
|
}
|
|
|
|
// Add invitation_link for pending members with token
|
|
if data, ok := result["data"].([]maps.MapStrAny); ok {
|
|
for i := range data {
|
|
member := data[i]
|
|
// Only generate invitation link for pending members with invitation_id and invitation_token
|
|
status, _ := member["status"].(string)
|
|
invitationID, _ := member["invitation_id"].(string)
|
|
invitationToken, _ := member["invitation_token"].(string)
|
|
|
|
if status == "pending" && invitationID != "" && invitationToken != "" {
|
|
// Build invitation link using the centralized helper function
|
|
invitationLink := buildTeamInvitationLink(invitationID, invitationToken, teamConfig, requestBaseURL)
|
|
member["invitation_link"] = invitationLink
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// memberGet handles the business logic for getting a specific team member
|
|
func memberGet(ctx context.Context, userID, teamID, memberID string) (maps.MapStrAny, error) {
|
|
// Check if user has access to the team (read permission: owner or member)
|
|
isOwner, isMember, err := checkTeamAccess(ctx, teamID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Allow access if user is owner or member
|
|
if !isOwner && !isMember {
|
|
return nil, fmt.Errorf("access denied: user is not a member of this team")
|
|
}
|
|
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Get member details using member_id (with all fields including robot config)
|
|
memberData, err := provider.GetMemberDetailByMemberID(ctx, memberID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("member not found: %w", err)
|
|
}
|
|
|
|
return memberData, nil
|
|
}
|
|
|
|
// memberCheckRobotEmail handles the business logic for checking if robot email exists globally
|
|
func memberCheckRobotEmail(ctx context.Context, userID, teamID, robotEmail string) (bool, error) {
|
|
// Check if user has access to the team (read permission: owner or member)
|
|
isOwner, isMember, err := checkTeamAccess(ctx, teamID, userID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Allow access if user is owner or member
|
|
if !isOwner && !isMember {
|
|
return false, fmt.Errorf("access denied: user is not a member of this team")
|
|
}
|
|
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Check if robot email exists globally (not limited to team)
|
|
exists, err := provider.MemberExistsByRobotEmail(ctx, robotEmail)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check robot email existence: %w", err)
|
|
}
|
|
|
|
return exists, nil
|
|
}
|
|
|
|
// memberCreateRobot handles the business logic for creating a robot member
|
|
func memberCreateRobot(ctx context.Context, userID, teamID string, robotData maps.MapStrAny) (string, error) {
|
|
// Check if user has access to the team (write permission: owner only)
|
|
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Only allow access if user is owner
|
|
if !isOwner {
|
|
return "", fmt.Errorf("access denied: only team owner can add robot members")
|
|
}
|
|
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Use CreateRobotMember method which handles robot-specific logic
|
|
memberID, err := provider.CreateRobotMember(ctx, teamID, robotData)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create robot member: %w", err)
|
|
}
|
|
|
|
return memberID, nil
|
|
}
|
|
|
|
// memberUpdateRobot handles the business logic for updating a robot member
|
|
func memberUpdateRobot(ctx context.Context, userID, teamID, memberID string, robotData maps.MapStrAny) error {
|
|
// Check if user has access to the team (write permission: owner only)
|
|
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Only allow access if user is owner
|
|
if !isOwner {
|
|
return fmt.Errorf("access denied: only team owner can update robot members")
|
|
}
|
|
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Use UpdateRobotMember method which handles robot-specific logic and validation
|
|
err = provider.UpdateRobotMember(ctx, memberID, robotData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update robot member: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// memberUpdate handles the business logic for updating a team member
|
|
func memberUpdate(ctx context.Context, userID, teamID, memberID string, updateData maps.MapStrAny) error {
|
|
// Check if user has access to the team (write permission: owner only)
|
|
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Only allow access if user is owner
|
|
if !isOwner {
|
|
return fmt.Errorf("access denied: only team owner can update members")
|
|
}
|
|
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Check if member exists using member_id
|
|
_, err = provider.GetMemberByMemberID(ctx, memberID)
|
|
if err != nil {
|
|
return fmt.Errorf("member not found: %w", err)
|
|
}
|
|
|
|
// Add updated_at timestamp
|
|
updateData["updated_at"] = time.Now()
|
|
|
|
// Update member using member_id
|
|
err = provider.UpdateMemberByMemberID(ctx, memberID, updateData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update member: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// memberGetProfile handles the business logic for getting member profile information
|
|
func memberGetProfile(ctx context.Context, requestUserID, teamID, memberUserID string) (maps.MapStrAny, error) {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Check if member exists using team_id and user_id
|
|
member, err := provider.GetMember(ctx, teamID, memberUserID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("member not found: %w", err)
|
|
}
|
|
|
|
// Verify member exists
|
|
if member == nil {
|
|
return nil, fmt.Errorf("member not found in the specified team")
|
|
}
|
|
|
|
// Check if the requesting user is the member themselves
|
|
// Only members can view their own profile
|
|
if memberUserID != requestUserID {
|
|
return nil, fmt.Errorf("access denied: you can only view your own profile")
|
|
}
|
|
|
|
// Return member profile data (display_name, bio, avatar, email)
|
|
profileData := maps.MapStrAny{
|
|
"user_id": memberUserID,
|
|
"team_id": teamID,
|
|
"display_name": member["display_name"],
|
|
"bio": member["bio"],
|
|
"avatar": member["avatar"],
|
|
"email": member["email"],
|
|
}
|
|
|
|
return profileData, nil
|
|
}
|
|
|
|
// memberUpdateProfile handles the business logic for updating member profile information
|
|
func memberUpdateProfile(ctx context.Context, requestUserID, teamID, memberUserID string, req UpdateMemberProfileRequest) error {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Check if member exists using team_id and user_id
|
|
member, err := provider.GetMember(ctx, teamID, memberUserID)
|
|
if err != nil {
|
|
return fmt.Errorf("member not found: %w", err)
|
|
}
|
|
|
|
// Verify member exists
|
|
if member == nil {
|
|
return fmt.Errorf("member not found in the specified team")
|
|
}
|
|
|
|
// Check if the requesting user is the member themselves
|
|
// Only members can update their own profile
|
|
if memberUserID != requestUserID {
|
|
return fmt.Errorf("access denied: you can only update your own profile")
|
|
}
|
|
|
|
// Build update data map (only include non-nil fields)
|
|
updateData := make(map[string]interface{})
|
|
|
|
if req.DisplayName != nil {
|
|
updateData["display_name"] = *req.DisplayName
|
|
}
|
|
if req.Bio != nil {
|
|
updateData["bio"] = *req.Bio
|
|
}
|
|
if req.Avatar != nil {
|
|
updateData["avatar"] = *req.Avatar
|
|
}
|
|
if req.Email != nil {
|
|
updateData["email"] = *req.Email
|
|
}
|
|
|
|
// If no fields to update, return error
|
|
if len(updateData) == 0 {
|
|
return fmt.Errorf("no fields to update")
|
|
}
|
|
|
|
// Add updated_at timestamp
|
|
updateData["updated_at"] = time.Now()
|
|
|
|
// Update member using team_id and user_id
|
|
err = provider.UpdateMember(ctx, teamID, memberUserID, updateData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update member profile: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// memberDelete handles the business logic for deleting a team member
|
|
func memberDelete(ctx context.Context, userID, teamID, memberID string) error {
|
|
// Check if user has access to the team (write permission: owner only)
|
|
isOwner, _, err := checkTeamAccess(ctx, teamID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Only allow access if user is owner
|
|
if !isOwner {
|
|
return fmt.Errorf("access denied: only team owner can remove members")
|
|
}
|
|
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Check if member exists using member_id
|
|
_, err = provider.GetMemberByMemberID(ctx, memberID)
|
|
if err != nil {
|
|
return fmt.Errorf("member not found: %w", err)
|
|
}
|
|
|
|
// Remove member using member_id
|
|
err = provider.RemoveMemberByMemberID(ctx, memberID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete member: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Private Helper Functions (internal use only)
|
|
|
|
// checkTeamAccess checks if user has access to the team
|
|
// Returns: (isOwner bool, isMember bool, error)
|
|
func checkTeamAccess(ctx context.Context, teamID, userID string) (bool, bool, error) {
|
|
// Get user provider instance
|
|
provider, err := getUserProvider()
|
|
if err != nil {
|
|
return false, false, fmt.Errorf("failed to get user provider: %w", err)
|
|
}
|
|
|
|
// Use UserProvider's CheckTeamAccess method - note parameter order: (ctx, teamID, userID)
|
|
return provider.CheckTeamAccess(ctx, teamID, userID)
|
|
}
|
|
|
|
// mapToMemberResponse converts a map to MemberResponse
|
|
func mapToMemberResponse(data maps.MapStr) MemberResponse {
|
|
member := MemberResponse{
|
|
ID: utils.ToInt64(data["id"]),
|
|
MemberID: utils.ToString(data["member_id"]),
|
|
TeamID: utils.ToString(data["team_id"]),
|
|
UserID: utils.ToString(data["user_id"]),
|
|
MemberType: utils.ToString(data["member_type"]),
|
|
DisplayName: utils.ToString(data["display_name"]),
|
|
Bio: utils.ToString(data["bio"]),
|
|
Avatar: utils.ToString(data["avatar"]),
|
|
Email: utils.ToString(data["email"]),
|
|
RobotEmail: utils.ToString(data["robot_email"]), // Globally unique email for robot members
|
|
RoleID: utils.ToString(data["role_id"]),
|
|
IsOwner: data["is_owner"], // Keep original type (int or bool)
|
|
Status: utils.ToString(data["status"]),
|
|
InvitationID: utils.ToString(data["invitation_id"]),
|
|
InvitedBy: utils.ToString(data["invited_by"]),
|
|
InvitedAt: utils.ToTimeString(data["invited_at"]),
|
|
InvitationToken: utils.ToString(data["invitation_token"]),
|
|
InvitationExpiresAt: utils.ToTimeString(data["invitation_expires_at"]),
|
|
JoinedAt: utils.ToTimeString(data["joined_at"]),
|
|
LastActiveAt: utils.ToTimeString(data["last_active_at"]),
|
|
LoginCount: utils.ToInt(data["login_count"]),
|
|
CreatedAt: utils.ToTimeString(data["created_at"]),
|
|
UpdatedAt: utils.ToTimeString(data["updated_at"]),
|
|
}
|
|
|
|
// Add settings if available
|
|
if settings, ok := data["settings"]; ok {
|
|
if memSettings, ok := settings.(*MemberSettings); ok {
|
|
member.Settings = memSettings
|
|
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
|
// Convert map to MemberSettings (for backward compatibility)
|
|
memSettings := &MemberSettings{
|
|
Notifications: utils.ToBool(settingsMap["notifications"]),
|
|
}
|
|
// Handle permissions array
|
|
if perms, ok := settingsMap["permissions"]; ok {
|
|
if permsSlice, ok := perms.([]interface{}); ok {
|
|
permissions := make([]string, 0, len(permsSlice))
|
|
for _, p := range permsSlice {
|
|
if permStr, ok := p.(string); ok {
|
|
permissions = append(permissions, permStr)
|
|
}
|
|
}
|
|
memSettings.Permissions = permissions
|
|
} else if permsStrSlice, ok := perms.([]string); ok {
|
|
memSettings.Permissions = permsStrSlice
|
|
}
|
|
}
|
|
member.Settings = memSettings
|
|
}
|
|
}
|
|
|
|
return member
|
|
}
|
|
|
|
// mapToMemberDetailResponse converts a map to MemberDetailResponse
|
|
func mapToMemberDetailResponse(data maps.MapStr) MemberDetailResponse {
|
|
member := MemberDetailResponse{
|
|
MemberResponse: mapToMemberResponse(data),
|
|
// Robot-specific fields
|
|
SystemPrompt: utils.ToString(data["system_prompt"]),
|
|
ManagerID: utils.ToString(data["manager_id"]),
|
|
LanguageModel: utils.ToString(data["language_model"]),
|
|
Workspace: utils.ToString(data["workspace"]),
|
|
CostLimit: utils.ToFloat64(data["cost_limit"]),
|
|
AutonomousMode: data["autonomous_mode"], // Keep original type (bool or string)
|
|
LastRobotActivity: utils.ToTimeString(data["last_robot_activity"]),
|
|
RobotStatus: utils.ToString(data["robot_status"]),
|
|
Notes: utils.ToString(data["notes"]),
|
|
}
|
|
|
|
// Handle authorized_senders array
|
|
if authorizedSenders, ok := data["authorized_senders"]; ok {
|
|
if sendersSlice, ok := authorizedSenders.([]interface{}); ok {
|
|
sendersList := make([]string, 0, len(sendersSlice))
|
|
for _, s := range sendersSlice {
|
|
if senderStr, ok := s.(string); ok {
|
|
sendersList = append(sendersList, senderStr)
|
|
}
|
|
}
|
|
member.AuthorizedSenders = sendersList
|
|
} else if sendersStrSlice, ok := authorizedSenders.([]string); ok {
|
|
member.AuthorizedSenders = sendersStrSlice
|
|
}
|
|
}
|
|
|
|
// Handle email_filter_rules array
|
|
if filterRules, ok := data["email_filter_rules"]; ok {
|
|
if rulesSlice, ok := filterRules.([]interface{}); ok {
|
|
rulesList := make([]string, 0, len(rulesSlice))
|
|
for _, r := range rulesSlice {
|
|
if ruleStr, ok := r.(string); ok {
|
|
rulesList = append(rulesList, ruleStr)
|
|
}
|
|
}
|
|
member.EmailFilterRules = rulesList
|
|
} else if rulesStrSlice, ok := filterRules.([]string); ok {
|
|
member.EmailFilterRules = rulesStrSlice
|
|
}
|
|
}
|
|
|
|
// Handle robot_config map
|
|
if robotConfig, ok := data["robot_config"]; ok {
|
|
if configMap, ok := robotConfig.(map[string]interface{}); ok {
|
|
member.RobotConfig = configMap
|
|
}
|
|
}
|
|
|
|
// Handle agents array
|
|
if agents, ok := data["agents"]; ok {
|
|
if agentsSlice, ok := agents.([]interface{}); ok {
|
|
agentsList := make([]string, 0, len(agentsSlice))
|
|
for _, a := range agentsSlice {
|
|
if agentStr, ok := a.(string); ok {
|
|
agentsList = append(agentsList, agentStr)
|
|
}
|
|
}
|
|
member.Agents = agentsList
|
|
} else if agentsStrSlice, ok := agents.([]string); ok {
|
|
member.Agents = agentsStrSlice
|
|
}
|
|
}
|
|
|
|
// Handle mcp_servers array
|
|
if mcpServers, ok := data["mcp_servers"]; ok {
|
|
if serversSlice, ok := mcpServers.([]interface{}); ok {
|
|
serversList := make([]string, 0, len(serversSlice))
|
|
for _, s := range serversSlice {
|
|
if serverStr, ok := s.(string); ok {
|
|
serversList = append(serversList, serverStr)
|
|
}
|
|
}
|
|
member.MCPServers = serversList
|
|
} else if serversStrSlice, ok := mcpServers.([]string); ok {
|
|
member.MCPServers = serversStrSlice
|
|
}
|
|
}
|
|
|
|
// Handle metadata map
|
|
if metadata, ok := data["metadata"]; ok {
|
|
if metadataMap, ok := metadata.(map[string]interface{}); ok {
|
|
member.Metadata = metadataMap
|
|
}
|
|
}
|
|
|
|
// Add user info if available (could be joined from user table)
|
|
if userInfo, ok := data["user_info"]; ok {
|
|
if userInfoMap, ok := userInfo.(map[string]interface{}); ok {
|
|
member.UserInfo = userInfoMap
|
|
}
|
|
}
|
|
|
|
return member
|
|
}
|