330 lines
11 KiB
Go
330 lines
11 KiB
Go
package externaladmin
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"hyapp-admin-server/internal/appctx"
|
|
"hyapp-admin-server/internal/integration/userclient"
|
|
"hyapp-admin-server/internal/middleware"
|
|
"hyapp-admin-server/internal/response"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
type externalInviteRequest struct {
|
|
TargetUserID FlexibleString `json:"targetUserId" binding:"required"`
|
|
ParentBDUserID FlexibleString `json:"parentBdUserId"`
|
|
AgencyID FlexibleString `json:"agencyId"`
|
|
AgencyName string `json:"agencyName"`
|
|
}
|
|
|
|
func (h *Handler) ListExternalTeamUsers(client userclient.ExternalTeamClient, fixedRole string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
scope, ok := CurrentScope(c)
|
|
if !ok || client == nil {
|
|
response.ServerError(c, "团队服务暂不可用")
|
|
return
|
|
}
|
|
role := strings.ToLower(strings.TrimSpace(fixedRole))
|
|
if role == "" {
|
|
role = strings.ToLower(strings.TrimSpace(c.Query("role")))
|
|
}
|
|
if !externalTeamRoleAllowed(c, role) {
|
|
response.Forbidden(c, "没有该团队列表权限")
|
|
return
|
|
}
|
|
page := positiveQueryInt(c.Query("page"), 1)
|
|
pageSize := positiveQueryInt(c.Query("pageSize"), 20)
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
result, err := client.ListExternalTeamUsers(c.Request.Context(), userclient.ListExternalTeamUsersRequest{
|
|
RequestID: middleware.CurrentRequestID(c), Caller: "hyapp-admin-external",
|
|
ScopeOwnerUserIDs: scope.OwnerUserIDs, RegionID: scope.RegionID, Role: role,
|
|
Status: strings.ToLower(strings.TrimSpace(c.DefaultQuery("status", "active"))),
|
|
Keyword: strings.TrimSpace(c.Query("keyword")), Page: page, PageSize: pageSize,
|
|
})
|
|
if err != nil {
|
|
writeExternalOwnerError(c, err, "获取团队用户失败")
|
|
return
|
|
}
|
|
if result == nil {
|
|
response.ServerError(c, "团队服务返回空结果")
|
|
return
|
|
}
|
|
response.OK(c, response.Page{Items: result.Users, Page: result.Page, PageSize: result.PageSize, Total: result.Total})
|
|
}
|
|
}
|
|
|
|
func (h *Handler) SearchExternalInvitationCandidate(client userclient.ExternalTeamClient) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
scope, ok := CurrentScope(c)
|
|
if !ok || client == nil {
|
|
response.ServerError(c, "邀请服务暂不可用")
|
|
return
|
|
}
|
|
invitationType := strings.ToLower(strings.TrimSpace(c.Query("invitationType")))
|
|
if !externalInvitePermissionAllowed(c, invitationType) {
|
|
response.Forbidden(c, "没有该邀请权限")
|
|
return
|
|
}
|
|
item, err := client.SearchExternalInvitationCandidate(c.Request.Context(), userclient.SearchExternalInvitationCandidateRequest{
|
|
RequestID: middleware.CurrentRequestID(c), Caller: "hyapp-admin-external",
|
|
OperatorUserID: scope.LinkedAppUserID, ExpectedRegionID: scope.RegionID,
|
|
InvitationType: invitationType, Keyword: strings.TrimSpace(c.Query("keyword")),
|
|
})
|
|
if err != nil {
|
|
writeExternalOwnerError(c, err, "未找到符合条件的同区域用户")
|
|
return
|
|
}
|
|
response.OK(c, item)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) CreateExternalBDInvitation(client userclient.ExternalTeamClient) gin.HandlerFunc {
|
|
return h.createExternalInvitation(client, "bd")
|
|
}
|
|
|
|
func (h *Handler) CreateExternalAgencyInvitation(client userclient.ExternalTeamClient) gin.HandlerFunc {
|
|
return h.createExternalInvitation(client, "agency")
|
|
}
|
|
|
|
func (h *Handler) CreateExternalHostInvitation(client userclient.ExternalTeamClient) gin.HandlerFunc {
|
|
return h.createExternalInvitation(client, "host")
|
|
}
|
|
|
|
func (h *Handler) createExternalInvitation(client userclient.ExternalTeamClient, invitationType string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
scope, ok := CurrentScope(c)
|
|
if !ok || client == nil {
|
|
response.ServerError(c, "邀请服务暂不可用")
|
|
return
|
|
}
|
|
var request externalInviteRequest
|
|
if err := c.ShouldBindJSON(&request); err != nil {
|
|
response.BadRequest(c, "邀请参数不正确")
|
|
return
|
|
}
|
|
targetUserID, err := positiveFlexibleID(request.TargetUserID)
|
|
if err != nil {
|
|
response.BadRequest(c, "目标用户 ID 不正确")
|
|
return
|
|
}
|
|
requestID := middleware.CurrentRequestID(c)
|
|
commandID := fmt.Sprintf("external-%s:%s:%d", invitationType, requestID, targetUserID)
|
|
var invitation *userclient.ExternalRoleInvitation
|
|
switch invitationType {
|
|
case "bd":
|
|
invitation, err = client.ExternalInviteBD(c.Request.Context(), userclient.ExternalInviteBDRequest{
|
|
RequestID: requestID, Caller: "hyapp-admin-external", CommandID: commandID,
|
|
OperatorUserID: scope.LinkedAppUserID, TargetUserID: targetUserID, ExpectedRegionID: scope.RegionID,
|
|
})
|
|
case "agency":
|
|
parentBDUserID, parseErr := positiveFlexibleID(request.ParentBDUserID)
|
|
if parseErr != nil || strings.TrimSpace(request.AgencyName) == "" {
|
|
response.BadRequest(c, "请选择团队 BD 并填写 Agency 名称")
|
|
return
|
|
}
|
|
invitation, err = client.ExternalInviteAgency(c.Request.Context(), userclient.ExternalInviteAgencyRequest{
|
|
RequestID: requestID, Caller: "hyapp-admin-external", CommandID: commandID,
|
|
OperatorUserID: scope.LinkedAppUserID, ParentBDUserID: parentBDUserID, TargetUserID: targetUserID,
|
|
AgencyName: strings.TrimSpace(request.AgencyName), ScopeOwnerUserIDs: scope.OwnerUserIDs, ExpectedRegionID: scope.RegionID,
|
|
})
|
|
case "host":
|
|
agencyID, parseErr := positiveFlexibleID(request.AgencyID)
|
|
if parseErr != nil {
|
|
response.BadRequest(c, "请选择团队 Agency")
|
|
return
|
|
}
|
|
invitation, err = client.ExternalInviteHost(c.Request.Context(), userclient.ExternalInviteHostRequest{
|
|
RequestID: requestID, Caller: "hyapp-admin-external", CommandID: commandID,
|
|
OperatorUserID: scope.LinkedAppUserID, AgencyID: agencyID, TargetUserID: targetUserID,
|
|
ScopeOwnerUserIDs: scope.OwnerUserIDs, ExpectedRegionID: scope.RegionID,
|
|
})
|
|
}
|
|
if err != nil {
|
|
writeExternalOwnerError(c, err, "创建邀请失败")
|
|
return
|
|
}
|
|
response.Created(c, invitation)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) RequireExternalTeamTarget(client userclient.ExternalTeamClient, param string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
scope, ok := CurrentScope(c)
|
|
targetUserID, err := strconv.ParseInt(strings.TrimSpace(c.Param(param)), 10, 64)
|
|
if !ok || client == nil || err != nil || targetUserID <= 0 {
|
|
response.Forbidden(c, "目标用户不在当前团队")
|
|
c.Abort()
|
|
return
|
|
}
|
|
allowed := false
|
|
for _, userStatus := range []string{"active", "banned", "disabled"} {
|
|
result, lookupErr := client.ListExternalTeamUsers(c.Request.Context(), userclient.ListExternalTeamUsersRequest{
|
|
RequestID: middleware.CurrentRequestID(c), Caller: "hyapp-admin-external-target-check",
|
|
ScopeOwnerUserIDs: scope.OwnerUserIDs, RegionID: scope.RegionID, Status: userStatus,
|
|
Keyword: strconv.FormatInt(targetUserID, 10), Page: 1, PageSize: 10,
|
|
})
|
|
if lookupErr != nil {
|
|
writeExternalOwnerError(c, lookupErr, "团队范围校验失败")
|
|
c.Abort()
|
|
return
|
|
}
|
|
for _, item := range result.Users {
|
|
if item.UserID == targetUserID && item.RegionID == scope.RegionID {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
if allowed {
|
|
break
|
|
}
|
|
}
|
|
if !allowed {
|
|
response.Forbidden(c, "目标用户不在当前团队或区域")
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// HydrateExternalRoomScope resolves business team members before entering roomadmin.
|
|
// Room owners are BD/Agency/Host users, not portal Local/经理/SuperAdmin account roots.
|
|
func (h *Handler) HydrateExternalRoomScope(client userclient.ExternalTeamClient) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
scope, ok := CurrentScope(c)
|
|
if !ok || client == nil {
|
|
response.ServerError(c, "团队服务暂不可用")
|
|
c.Abort()
|
|
return
|
|
}
|
|
const maxRoomScopeUsers = 5000
|
|
seen := make(map[int64]struct{})
|
|
teamUserIDs := make([]int64, 0)
|
|
for _, userStatus := range []string{"active", "banned", "disabled"} {
|
|
for page := 1; ; page++ {
|
|
result, err := client.ListExternalTeamUsers(c.Request.Context(), userclient.ListExternalTeamUsersRequest{
|
|
RequestID: middleware.CurrentRequestID(c), Caller: "hyapp-admin-external-room-scope",
|
|
ScopeOwnerUserIDs: scope.OwnerUserIDs, RegionID: scope.RegionID, Status: userStatus,
|
|
Page: page, PageSize: 200,
|
|
})
|
|
if err != nil {
|
|
writeExternalOwnerError(c, err, "房间团队范围校验失败")
|
|
c.Abort()
|
|
return
|
|
}
|
|
if result == nil {
|
|
response.ServerError(c, "团队服务返回空结果")
|
|
c.Abort()
|
|
return
|
|
}
|
|
for _, item := range result.Users {
|
|
if item.UserID <= 0 || item.RegionID != scope.RegionID {
|
|
continue
|
|
}
|
|
if _, exists := seen[item.UserID]; exists {
|
|
continue
|
|
}
|
|
seen[item.UserID] = struct{}{}
|
|
teamUserIDs = append(teamUserIDs, item.UserID)
|
|
if len(teamUserIDs) > maxRoomScopeUsers {
|
|
response.Forbidden(c, "当前团队规模超过房间管理上限")
|
|
c.Abort()
|
|
return
|
|
}
|
|
}
|
|
if len(result.Users) == 0 || result.PageSize <= 0 || int64(page*result.PageSize) >= result.Total {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
requestScope := appctx.ExternalScope{
|
|
AccountID: scope.AccountID, LinkedAppUserID: scope.LinkedAppUserID,
|
|
IdentityType: scope.IdentityType, RegionID: scope.RegionID, OwnerUserIDs: teamUserIDs,
|
|
}
|
|
c.Request = c.Request.WithContext(appctx.WithExternalScope(c.Request.Context(), requestScope))
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
func positiveFlexibleID(value FlexibleString) (int64, error) {
|
|
parsed, err := strconv.ParseInt(strings.TrimSpace(string(value)), 10, 64)
|
|
if err != nil || parsed <= 0 {
|
|
return 0, ErrInvalidInput
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func positiveQueryInt(raw string, fallback int) int {
|
|
value, err := strconv.Atoi(strings.TrimSpace(raw))
|
|
if err != nil || value <= 0 {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func externalTeamRoleAllowed(c *gin.Context, role string) bool {
|
|
switch role {
|
|
case "":
|
|
return principalHasAnyPermission(c, "user:list", "user:update", "user:ban", "user-ban:list", "user:unban", "user-level:grant")
|
|
case "bd":
|
|
// Agency invitations must select an existing BD even when the account was
|
|
// intentionally granted agency:invite without the standalone BD list menu.
|
|
return principalHasAnyPermission(c, "bd:list", "bd:invite", "agency:invite")
|
|
case "agency":
|
|
// Host invitations have the same bounded parent-selector dependency on Agency.
|
|
return principalHasAnyPermission(c, "agency:list", "agency:invite", "host:invite")
|
|
case "host":
|
|
return principalHasAnyPermission(c, "host:list", "host:invite")
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func externalInvitePermissionAllowed(c *gin.Context, invitationType string) bool {
|
|
return principalHasAnyPermission(c, invitationType+":invite")
|
|
}
|
|
|
|
func principalHasAnyPermission(c *gin.Context, permissions ...string) bool {
|
|
principal, ok := CurrentPrincipal(c)
|
|
if !ok {
|
|
return false
|
|
}
|
|
assigned := make(map[string]struct{}, len(principal.Permissions))
|
|
for _, permission := range principal.Permissions {
|
|
assigned[permission] = struct{}{}
|
|
}
|
|
for _, permission := range permissions {
|
|
if _, exists := assigned[permission]; exists {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func writeExternalOwnerError(c *gin.Context, err error, fallback string) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
code := status.Code(err)
|
|
switch {
|
|
case code == codes.InvalidArgument:
|
|
response.BadRequest(c, status.Convert(err).Message())
|
|
case code == codes.NotFound:
|
|
response.NotFound(c, status.Convert(err).Message())
|
|
case code == codes.PermissionDenied || code == codes.FailedPrecondition:
|
|
response.Forbidden(c, status.Convert(err).Message())
|
|
case errors.Is(err, ErrInvalidInput):
|
|
response.BadRequest(c, fallback)
|
|
default:
|
|
response.ServerError(c, fallback)
|
|
}
|
|
}
|