Add team ownership and membership checks in user management
- Introduced IsTeamOwner and IsTeamMember methods to verify user ownership and membership status within a team. - Added CheckTeamAccess method to assess a user's access level, returning ownership and membership status. - Enhanced user types with new MemberResponse and MemberDetailResponse structures for improved member management. - Updated routing for team member management to utilize new handler functions for clarity and consistency.
This commit is contained in:
parent
9aff88d2fc
commit
35afc1d58c
5 changed files with 1941 additions and 5 deletions
|
|
@ -348,3 +348,51 @@ func (u *DefaultUser) TransferTeamOwnership(ctx context.Context, teamID string,
|
|||
|
||||
return u.UpdateTeam(ctx, teamID, updateData)
|
||||
}
|
||||
|
||||
// IsTeamOwner checks if a user is the owner of a team
|
||||
func (u *DefaultUser) IsTeamOwner(ctx context.Context, teamID string, userID string) (bool, error) {
|
||||
teamData, err := u.GetTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get team: %w", err)
|
||||
}
|
||||
|
||||
ownerID, ok := teamData["owner_id"].(string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("invalid owner_id type in team data")
|
||||
}
|
||||
|
||||
return ownerID == userID, nil
|
||||
}
|
||||
|
||||
// IsTeamMember checks if a user is a member of a team (includes owner)
|
||||
func (u *DefaultUser) IsTeamMember(ctx context.Context, teamID string, userID string) (bool, error) {
|
||||
// First check if user is the owner
|
||||
isOwner, err := u.IsTeamOwner(ctx, teamID, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if isOwner {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Then check if user is a member
|
||||
return u.MemberExists(ctx, teamID, userID)
|
||||
}
|
||||
|
||||
// CheckTeamAccess checks user's access level to a team
|
||||
// Returns: (isOwner bool, isMember bool, error)
|
||||
func (u *DefaultUser) CheckTeamAccess(ctx context.Context, teamID string, userID string) (bool, bool, error) {
|
||||
// Check if user is the owner
|
||||
isOwner, err := u.IsTeamOwner(ctx, teamID, userID)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
// Check if user is a member (this will return true for owner as well, but we already know that)
|
||||
isMember, err := u.IsTeamMember(ctx, teamID, userID)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
return isOwner, isMember, nil
|
||||
}
|
||||
|
|
|
|||
1024
openapi/tests/user/member_test.go
Normal file
1024
openapi/tests/user/member_test.go
Normal file
File diff suppressed because it is too large
Load diff
822
openapi/user/member.go
Normal file
822
openapi/user/member.go
Normal file
|
|
@ -0,0 +1,822 @@
|
|||
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/response"
|
||||
)
|
||||
|
||||
// Member Management Handlers
|
||||
|
||||
// GinMemberList handles GET /teams/:team_id/members - Get team members
|
||||
func GinMemberList(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("team_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
|
||||
c.JSON(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("team_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)
|
||||
c.JSON(http.StatusOK, member)
|
||||
}
|
||||
|
||||
// GinMemberCreateDirect handles POST /teams/:team_id/members/direct - Add member directly
|
||||
func GinMemberCreateDirect(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("team_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 CreateMemberRequest
|
||||
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 member data
|
||||
memberData := maps.MapStrAny{
|
||||
"user_id": req.UserID,
|
||||
"member_type": req.MemberType,
|
||||
"role_id": req.RoleID,
|
||||
}
|
||||
|
||||
// Add settings if provided
|
||||
if req.Settings != nil {
|
||||
memberData["settings"] = req.Settings
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
memberID, err := memberCreateDirect(c.Request.Context(), authInfo.UserID, teamID, memberData)
|
||||
if err != nil {
|
||||
log.Error("Failed to create 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") {
|
||||
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 member",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Return created member ID
|
||||
c.JSON(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("team_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
|
||||
}
|
||||
|
||||
c.JSON(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("team_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
|
||||
}
|
||||
|
||||
c.JSON(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
|
||||
}
|
||||
|
||||
// ProcessMemberCreateDirect user.member.create Member create processor
|
||||
// Args[0] string: team_id
|
||||
// Args[1] map: Member data {"user_id": "user123", "member_type": "user", "role_id": "member", "settings": {...}}
|
||||
// Return: map: {"member_id": "created_member_id"}
|
||||
func ProcessMemberCreateDirect(process *process.Process) interface{} {
|
||||
process.ValidateArgNums(2)
|
||||
|
||||
// Get user_id from session
|
||||
userIDStr := GetUserIDFromSession(process)
|
||||
|
||||
teamID := process.ArgsString(0)
|
||||
memberData := maps.MapStrAny(process.ArgsMap(1))
|
||||
|
||||
if teamID == "" {
|
||||
exception.New("team_id is required", 400).Throw()
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if _, ok := memberData["user_id"]; !ok {
|
||||
exception.New("user_id is required", 400).Throw()
|
||||
}
|
||||
if _, ok := memberData["role_id"]; !ok {
|
||||
exception.New("role_id is required", 400).Throw()
|
||||
}
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Call business logic
|
||||
memberID, err := memberCreateDirect(ctx, userIDStr, teamID, memberData)
|
||||
if err != nil {
|
||||
exception.New("failed to create member: %s", 500, err.Error()).Throw()
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"member_id": memberID,
|
||||
}
|
||||
}
|
||||
|
||||
// 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, userID, teamID)
|
||||
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, userID, teamID)
|
||||
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
|
||||
}
|
||||
|
||||
// memberCreateDirect handles the business logic for creating a team member directly
|
||||
func memberCreateDirect(ctx context.Context, userID, teamID string, memberData maps.MapStrAny) (int64, error) {
|
||||
// Check if user has access to the team (write permission: owner only)
|
||||
isOwner, _, err := checkTeamAccess(ctx, userID, teamID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Only allow access if user is owner
|
||||
if !isOwner {
|
||||
return 0, fmt.Errorf("access denied: only team owner can add members")
|
||||
}
|
||||
|
||||
// Get user provider instance
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get user provider: %w", err)
|
||||
}
|
||||
|
||||
// Check if member already exists
|
||||
memberUserID := toString(memberData["user_id"])
|
||||
exists, err := provider.MemberExists(ctx, teamID, memberUserID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to check member existence: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return 0, fmt.Errorf("member already exists in this team")
|
||||
}
|
||||
|
||||
// Set team ID and default values
|
||||
memberData["team_id"] = teamID
|
||||
if memberData["member_type"] == nil || memberData["member_type"] == "" {
|
||||
memberData["member_type"] = "user"
|
||||
}
|
||||
memberData["status"] = "active"
|
||||
memberData["joined_at"] = time.Now()
|
||||
memberData["created_at"] = time.Now()
|
||||
memberData["updated_at"] = time.Now()
|
||||
|
||||
// Create member
|
||||
memberID, err := provider.CreateMember(ctx, memberData)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create 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, userID, teamID)
|
||||
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, userID, teamID)
|
||||
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, userID, teamID 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
|
||||
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 settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||
member.Settings = settingsMap
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -238,3 +238,45 @@ type UpdateTeamRequest struct {
|
|||
Description string `json:"description,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
// ==== Member API Types ====
|
||||
|
||||
// MemberResponse represents a team member in API responses
|
||||
type MemberResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
TeamID string `json:"team_id"`
|
||||
UserID string `json:"user_id"`
|
||||
MemberType string `json:"member_type"`
|
||||
RoleID string `json:"role_id"`
|
||||
Status string `json:"status"`
|
||||
InvitedBy string `json:"invited_by,omitempty"`
|
||||
InvitedAt string `json:"invited_at,omitempty"`
|
||||
JoinedAt string `json:"joined_at,omitempty"`
|
||||
LastActivity string `json:"last_activity,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// MemberDetailResponse represents detailed member information
|
||||
type MemberDetailResponse struct {
|
||||
MemberResponse
|
||||
// Add additional fields that are only included in detailed responses
|
||||
UserInfo map[string]interface{} `json:"user_info,omitempty"`
|
||||
}
|
||||
|
||||
// CreateMemberRequest represents the request to add a member directly
|
||||
type CreateMemberRequest struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
MemberType string `json:"member_type,omitempty"` // "user" or "robot"
|
||||
RoleID string `json:"role_id" binding:"required"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateMemberRequest represents the request to update a member
|
||||
type UpdateMemberRequest struct {
|
||||
RoleID string `json:"role_id,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
LastActivity string `json:"last_activity,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,11 +62,11 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
team.DELETE("/:team_id", GinTeamDelete) // Delete user team
|
||||
|
||||
// Member Management
|
||||
team.GET("/:team_id/members", placeholder) // Get user team members
|
||||
team.GET("/:team_id/members/:member_id", placeholder) // Get user team member details
|
||||
team.POST("/:team_id/members/direct", placeholder) // Add member directly (for bots/system)
|
||||
team.PUT("/:team_id/members/:member_id", placeholder) // Update user team member
|
||||
team.DELETE("/:team_id/members/:member_id", placeholder) // Remove user team member
|
||||
team.GET("/:team_id/members", GinMemberList) // Get user team members
|
||||
team.GET("/:team_id/members/:member_id", GinMemberGet) // Get user team member details
|
||||
team.POST("/:team_id/members/direct", GinMemberCreateDirect) // Add member directly (for bots/system)
|
||||
team.PUT("/:team_id/members/:member_id", GinMemberUpdate) // Update user team member
|
||||
team.DELETE("/:team_id/members/:member_id", GinMemberDelete) // Remove user team member
|
||||
|
||||
// Member Invitation Management
|
||||
team.POST("/:team_id/invitations", placeholder) // Send team invitation
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue