- Introduced member_id as a unique identifier for members, improving data integrity and consistency across member operations. - Updated member-related functions to utilize member_id instead of internal IDs, enhancing API usability and clarity. - Implemented email existence checks for team members, allowing for better validation during member creation and updates. - Refactored existing tests to accommodate changes in member identification and email validation, ensuring comprehensive coverage and reliability. - Added new API endpoint for checking if an email exists within a team, improving user experience during member management.
896 lines
27 KiB
Go
896 lines
27 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"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"
|
|
)
|
|
|
|
// Member Management Handlers
|
|
|
|
// GinMemberList handles GET /teams/:team_id/members - Get team members
|
|
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 pagination parameters
|
|
page := 1
|
|
pagesize := 20
|
|
|
|
if p := c.Query("page"); p != "" {
|
|
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
|
|
page = parsed
|
|
}
|
|
}
|
|
|
|
if ps := c.Query("pagesize"); ps != "" {
|
|
if parsed, err := strconv.Atoi(ps); err == nil && parsed > 0 && parsed <= 100 {
|
|
pagesize = parsed
|
|
}
|
|
}
|
|
|
|
// Call business logic
|
|
result, err := memberList(c.Request.Context(), authInfo.UserID, teamID, page, pagesize, c.Query("status"))
|
|
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 {
|
|
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)
|
|
}
|
|
|
|
// GinMemberCheckEmail handles GET /api/user/teams/:id/members/check?email=xxx - Check if email exists in team
|
|
func GinMemberCheckEmail(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
|
|
}
|
|
|
|
email := c.Query("email")
|
|
if email == "" {
|
|
errorResp := &response.ErrorResponse{
|
|
Code: response.ErrInvalidRequest.Code,
|
|
ErrorDescription: "Email query parameter is required",
|
|
}
|
|
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
|
return
|
|
}
|
|
|
|
// Call business logic
|
|
exists, err := memberCheckEmail(c.Request.Context(), authInfo.UserID, teamID, email)
|
|
if err != nil {
|
|
log.Error("Failed to check member 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 member email: %v", err),
|
|
}
|
|
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Return result
|
|
result := map[string]interface{}{
|
|
"exists": exists,
|
|
"email": email,
|
|
"team_id": teamID,
|
|
}
|
|
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,
|
|
"email": req.Email,
|
|
"bio": req.Bio,
|
|
"role_id": req.RoleID,
|
|
"system_prompt": req.SystemPrompt,
|
|
"autonomous_mode": toBool(req.AutonomousMode),
|
|
}
|
|
|
|
// Add optional fields
|
|
if req.ManagerID != "" {
|
|
baseData["manager_id"] = req.ManagerID
|
|
}
|
|
if req.LanguageModel != "" {
|
|
baseData["language_model"] = req.LanguageModel
|
|
}
|
|
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})
|
|
}
|
|
|
|
// 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"})
|
|
}
|
|
|
|
// 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 {"status": "active", "page": 1, "pagesize": 20}
|
|
// 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)
|
|
|
|
// Parse pagination
|
|
page := 1
|
|
pagesize := 20
|
|
|
|
if p, ok := queryMap["page"]; ok {
|
|
if pageInt, ok := p.(int); ok && pageInt > 0 {
|
|
page = pageInt
|
|
}
|
|
}
|
|
|
|
if ps, ok := queryMap["pagesize"]; ok {
|
|
if pagesizeInt, ok := ps.(int); ok && pagesizeInt > 0 && pagesizeInt <= 100 {
|
|
pagesize = pagesizeInt
|
|
}
|
|
}
|
|
|
|
// Get status filter
|
|
status := ""
|
|
if s, ok := queryMap["status"].(string); ok {
|
|
status = s
|
|
}
|
|
|
|
// Get context
|
|
ctx := process.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
// Call business logic
|
|
result, err := memberList(ctx, userIDStr, teamID, page, pagesize, status)
|
|
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",
|
|
}
|
|
}
|
|
|
|
// 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
|
|
func memberList(ctx context.Context, userID, teamID string, page, pagesize int, status 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)
|
|
}
|
|
|
|
// Build query parameters
|
|
param := model.QueryParam{
|
|
Wheres: []model.QueryWhere{
|
|
{Column: "team_id", Value: teamID},
|
|
},
|
|
Orders: []model.QueryOrder{
|
|
{Column: "joined_at", Option: "desc"},
|
|
{Column: "created_at", Option: "desc"},
|
|
},
|
|
}
|
|
|
|
// Add status filter if provided
|
|
if status != "" {
|
|
param.Wheres = append(param.Wheres, model.QueryWhere{
|
|
Column: "status",
|
|
Value: status,
|
|
})
|
|
}
|
|
|
|
// Get paginated members
|
|
result, err := provider.PaginateMembers(ctx, param, page, pagesize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to retrieve members: %w", err)
|
|
}
|
|
|
|
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 team_id + user_id (business keys)
|
|
// memberID parameter is actually user_id in the context of team_id
|
|
memberData, err := provider.GetMember(ctx, teamID, memberID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("member not found: %w", err)
|
|
}
|
|
|
|
return memberData, nil
|
|
}
|
|
|
|
// memberCheckEmail handles the business logic for checking if member exists by team_id and email
|
|
func memberCheckEmail(ctx context.Context, userID, teamID, email 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 member exists by team_id and email
|
|
exists, err := provider.MemberExistsByTeamEmail(ctx, teamID, email)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to check member 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
|
|
}
|
|
|
|
// memberUpdate handles the business logic for updating a team member
|
|
func memberUpdate(ctx context.Context, userID, teamID, memberUserID 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 team_id + user_id (business keys)
|
|
_, err = provider.GetMember(ctx, teamID, memberUserID)
|
|
if err != nil {
|
|
return fmt.Errorf("member not found: %w", err)
|
|
}
|
|
|
|
// Add updated_at timestamp
|
|
updateData["updated_at"] = time.Now()
|
|
|
|
// Update member using team_id + user_id
|
|
err = provider.UpdateMember(ctx, teamID, memberUserID, updateData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update member: %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 team_id + user_id (business keys)
|
|
// memberID parameter is actually user_id in the context of team_id
|
|
_, err = provider.GetMember(ctx, teamID, memberID)
|
|
if err != nil {
|
|
return fmt.Errorf("member not found: %w", err)
|
|
}
|
|
|
|
// Remove member using team_id + user_id
|
|
err = provider.RemoveMember(ctx, teamID, 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: toInt64(data["id"]),
|
|
TeamID: toString(data["team_id"]),
|
|
UserID: toString(data["user_id"]),
|
|
MemberType: toString(data["member_type"]),
|
|
RoleID: toString(data["role_id"]),
|
|
Status: toString(data["status"]),
|
|
InvitedBy: toString(data["invited_by"]),
|
|
InvitedAt: toTimeString(data["invited_at"]),
|
|
JoinedAt: toTimeString(data["joined_at"]),
|
|
LastActivity: toTimeString(data["last_activity"]),
|
|
CreatedAt: toTimeString(data["created_at"]),
|
|
UpdatedAt: 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: 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),
|
|
}
|
|
|
|
// 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
|
|
}
|