b8c45b173e
The share-time validation forced every agent bound to a knowledge base to have a rerank model configured, which blocked Wiki-type agents (`wiki_search` / `wiki_read_page` / …) from being shared even though they never invoke the reranker at runtime. Align the check with the runtime logic in `session_agent_qa.go`: only require a rerank model when the agent's allowed_tools include `knowledge_search` (or rely on the default tool set, which contains it). Wiki-only agents can now be shared without a rerank model; hybrid / RAG agents still enforce the requirement. Also update the handler error message to reflect the new rule.
1810 lines
62 KiB
Go
1810 lines
62 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/Tencent/WeKnora/internal/application/service"
|
|
apperrors "github.com/Tencent/WeKnora/internal/errors"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
secutils "github.com/Tencent/WeKnora/internal/utils"
|
|
)
|
|
|
|
// OrganizationHandler implements HTTP request handlers for organization management
|
|
type OrganizationHandler struct {
|
|
orgService interfaces.OrganizationService
|
|
shareService interfaces.KBShareService
|
|
agentShareService interfaces.AgentShareService
|
|
customAgentService interfaces.CustomAgentService
|
|
userService interfaces.UserService
|
|
kbService interfaces.KnowledgeBaseService
|
|
knowledgeRepo interfaces.KnowledgeRepository
|
|
chunkRepo interfaces.ChunkRepository
|
|
}
|
|
|
|
// NewOrganizationHandler creates a new organization handler
|
|
func NewOrganizationHandler(
|
|
orgService interfaces.OrganizationService,
|
|
shareService interfaces.KBShareService,
|
|
agentShareService interfaces.AgentShareService,
|
|
customAgentService interfaces.CustomAgentService,
|
|
userService interfaces.UserService,
|
|
kbService interfaces.KnowledgeBaseService,
|
|
knowledgeRepo interfaces.KnowledgeRepository,
|
|
chunkRepo interfaces.ChunkRepository,
|
|
) *OrganizationHandler {
|
|
return &OrganizationHandler{
|
|
orgService: orgService,
|
|
shareService: shareService,
|
|
agentShareService: agentShareService,
|
|
customAgentService: customAgentService,
|
|
userService: userService,
|
|
kbService: kbService,
|
|
knowledgeRepo: knowledgeRepo,
|
|
chunkRepo: chunkRepo,
|
|
}
|
|
}
|
|
|
|
// CreateOrganization creates a new organization
|
|
// @Summary 创建组织
|
|
// @Description 创建新的组织,创建者自动成为管理员
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body types.CreateOrganizationRequest true "组织信息"
|
|
// @Success 201 {object} map[string]interface{}
|
|
// @Failure 400 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations [post]
|
|
func (h *OrganizationHandler) CreateOrganization(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
var req types.CreateOrganizationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Errorf(ctx, "Invalid request parameters: %v", err)
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
org, err := h.orgService.CreateOrganization(ctx, userID, tenantID, &req)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to create organization: %v", err)
|
|
if errors.Is(err, service.ErrInvalidValidityDays) {
|
|
c.Error(apperrors.NewValidationError(err.Error()))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewInternalServerError("Failed to create organization").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
logger.Infof(ctx, "Organization created: %s", org.ID)
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"success": true,
|
|
"data": h.toOrgResponse(ctx, org, userID),
|
|
})
|
|
}
|
|
|
|
// GetOrganization gets an organization by ID
|
|
// @Summary 获取组织详情
|
|
// @Description 根据ID获取组织详情
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 404 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id} [get]
|
|
func (h *OrganizationHandler) GetOrganization(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
org, err := h.orgService.GetOrganization(ctx, orgID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to get organization: %v", err)
|
|
c.Error(apperrors.NewNotFoundError("Organization not found"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": h.toOrgResponse(ctx, org, userID),
|
|
})
|
|
}
|
|
|
|
// ListMyOrganizations lists organizations that the current user belongs to.
|
|
// Response includes resource_counts (per-org KB/agent counts) for list sidebar so frontend does not need a separate GET /me/resource-counts.
|
|
// @Summary 获取我的组织列表
|
|
// @Description 获取当前用户所属的所有组织,并附带各空间内知识库/智能体数量
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Success 200 {object} types.ListOrganizationsResponse
|
|
// @Security Bearer
|
|
// @Router /organizations [get]
|
|
func (h *OrganizationHandler) ListMyOrganizations(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
orgs, err := h.orgService.ListUserOrganizations(ctx, userID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list organizations: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list organizations").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
response := make([]types.OrganizationResponse, 0, len(orgs))
|
|
for _, org := range orgs {
|
|
response = append(response, h.toOrgResponse(ctx, org, userID))
|
|
}
|
|
|
|
resp := types.ListOrganizationsResponse{
|
|
Organizations: response,
|
|
Total: int64(len(response)),
|
|
}
|
|
// 附带各空间资源数量,供知识库/智能体列表页侧栏展示
|
|
resp.ResourceCounts = h.buildResourceCountsByOrg(ctx, orgs, userID, tenantID)
|
|
if resp.ResourceCounts != nil {
|
|
// 补齐未出现在 map 中的 org 为 0
|
|
for _, o := range orgs {
|
|
if _, ok := resp.ResourceCounts.KnowledgeBases.ByOrganization[o.ID]; !ok {
|
|
resp.ResourceCounts.KnowledgeBases.ByOrganization[o.ID] = 0
|
|
}
|
|
if _, ok := resp.ResourceCounts.Agents.ByOrganization[o.ID]; !ok {
|
|
resp.ResourceCounts.Agents.ByOrganization[o.ID] = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": resp,
|
|
})
|
|
}
|
|
|
|
// buildResourceCountsByOrg 返回各空间内知识库数与智能体数,供 ListMyOrganizations 和侧栏使用;失败时返回 nil。
|
|
// 使用批量接口:一次拉取所有空间的直接共享 KB ID、一次拉取所有空间的智能体列表,再在内存中按空间合并计数。
|
|
func (h *OrganizationHandler) buildResourceCountsByOrg(ctx context.Context, orgs []*types.Organization, userID string, tenantID uint64) *types.ResourceCountsByOrgResponse {
|
|
orgIDs := make([]string, 0, len(orgs))
|
|
for _, o := range orgs {
|
|
orgIDs = append(orgIDs, o.ID)
|
|
}
|
|
agentCounts, err := h.agentShareService.CountByOrganizations(ctx, orgIDs)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "buildResourceCountsByOrg CountByOrganizations: %v", err)
|
|
return nil
|
|
}
|
|
directKBIDsByOrg, err := h.shareService.ListSharedKnowledgeBaseIDsByOrganizations(ctx, orgIDs, userID)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "buildResourceCountsByOrg ListSharedKnowledgeBaseIDsByOrganizations: %v", err)
|
|
return nil
|
|
}
|
|
agentListByOrg, err := h.agentShareService.ListSharedAgentsInOrganizations(ctx, orgIDs, userID, tenantID)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "buildResourceCountsByOrg ListSharedAgentsInOrganizations: %v", err)
|
|
return nil
|
|
}
|
|
byOrgKB := make(map[string]int)
|
|
tenantKBCache := make(map[uint64][]string) // cache ListKnowledgeBasesByTenantID by tenantID
|
|
for _, o := range orgs {
|
|
oid := o.ID
|
|
directIDs := directKBIDsByOrg[oid]
|
|
directSet := make(map[string]bool)
|
|
for _, id := range directIDs {
|
|
directSet[id] = true
|
|
}
|
|
count := len(directIDs)
|
|
for _, item := range agentListByOrg[oid] {
|
|
if item.Agent == nil {
|
|
continue
|
|
}
|
|
agent := item.Agent
|
|
mode := agent.Config.KBSelectionMode
|
|
if mode == "none" {
|
|
continue
|
|
}
|
|
var kbIDs []string
|
|
switch mode {
|
|
case "selected":
|
|
if len(agent.Config.KnowledgeBases) == 0 {
|
|
continue
|
|
}
|
|
kbIDs = agent.Config.KnowledgeBases
|
|
case "all":
|
|
tid := agent.TenantID
|
|
if _, ok := tenantKBCache[tid]; !ok {
|
|
kbs, err := h.kbService.ListKnowledgeBasesByTenantID(ctx, tid)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "ListKnowledgeBasesByTenantID tenant %d: %v", tid, err)
|
|
tenantKBCache[tid] = nil
|
|
continue
|
|
}
|
|
ids := make([]string, 0, len(kbs))
|
|
for _, kb := range kbs {
|
|
if kb != nil && kb.ID != "" {
|
|
ids = append(ids, kb.ID)
|
|
}
|
|
}
|
|
tenantKBCache[tid] = ids
|
|
}
|
|
kbIDs = tenantKBCache[tid]
|
|
default:
|
|
if len(agent.Config.KnowledgeBases) > 0 {
|
|
kbIDs = agent.Config.KnowledgeBases
|
|
}
|
|
}
|
|
for _, kbID := range kbIDs {
|
|
if kbID != "" && !directSet[kbID] {
|
|
directSet[kbID] = true
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
byOrgKB[oid] = count
|
|
}
|
|
byOrgAgent := make(map[string]int)
|
|
for _, o := range orgs {
|
|
byOrgAgent[o.ID] = 0
|
|
}
|
|
for id, n := range agentCounts {
|
|
byOrgAgent[id] = int(n)
|
|
}
|
|
return &types.ResourceCountsByOrgResponse{
|
|
KnowledgeBases: struct {
|
|
ByOrganization map[string]int `json:"by_organization"`
|
|
}{ByOrganization: byOrgKB},
|
|
Agents: struct {
|
|
ByOrganization map[string]int `json:"by_organization"`
|
|
}{ByOrganization: byOrgAgent},
|
|
}
|
|
}
|
|
|
|
// UpdateOrganization updates an organization
|
|
// @Summary 更新组织
|
|
// @Description 更新组织信息(需要管理员权限)
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Param request body types.UpdateOrganizationRequest true "更新信息"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id} [put]
|
|
func (h *OrganizationHandler) UpdateOrganization(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
var req types.UpdateOrganizationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
org, err := h.orgService.UpdateOrganization(ctx, orgID, userID, &req)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to update organization: %v", err)
|
|
if errors.Is(err, service.ErrInvalidValidityDays) {
|
|
c.Error(apperrors.NewValidationError(err.Error()))
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrOrgMemberLimitTooLow) {
|
|
c.Error(apperrors.NewValidationError("当前成员数已超过新的上限,请先移除成员或设置更大的上限"))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewForbiddenError("Permission denied or organization not found"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": h.toOrgResponse(ctx, org, userID),
|
|
})
|
|
}
|
|
|
|
// DeleteOrganization deletes an organization
|
|
// @Summary 删除组织
|
|
// @Description 删除组织(仅组织创建者可操作)
|
|
// @Tags 组织管理
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id} [delete]
|
|
func (h *OrganizationHandler) DeleteOrganization(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
if err := h.orgService.DeleteOrganization(ctx, orgID, userID); err != nil {
|
|
logger.Errorf(ctx, "Failed to delete organization: %v", err)
|
|
c.Error(apperrors.NewForbiddenError("Permission denied or organization not found"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Organization deleted successfully",
|
|
})
|
|
}
|
|
|
|
// ListMembers lists all members of an organization
|
|
// @Summary 获取组织成员列表
|
|
// @Description 获取组织的所有成员
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} types.ListMembersResponse
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/members [get]
|
|
func (h *OrganizationHandler) ListMembers(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
|
|
members, err := h.orgService.ListMembers(ctx, orgID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list members: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list members").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
response := make([]types.OrganizationMemberResponse, 0, len(members))
|
|
for _, m := range members {
|
|
resp := types.OrganizationMemberResponse{
|
|
ID: m.ID,
|
|
UserID: m.UserID,
|
|
Role: string(m.Role),
|
|
TenantID: m.TenantID,
|
|
JoinedAt: m.CreatedAt,
|
|
}
|
|
if m.User != nil {
|
|
resp.Username = m.User.Username
|
|
resp.Email = m.User.Email
|
|
resp.Avatar = m.User.Avatar
|
|
}
|
|
response = append(response, resp)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": types.ListMembersResponse{
|
|
Members: response,
|
|
Total: int64(len(response)),
|
|
},
|
|
})
|
|
}
|
|
|
|
// UpdateMemberRole updates a member's role
|
|
// @Summary 更新成员角色
|
|
// @Description 更新组织成员的角色(需要管理员权限)
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Param user_id path string true "用户ID"
|
|
// @Param request body types.UpdateMemberRoleRequest true "角色信息"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/members/{user_id} [put]
|
|
func (h *OrganizationHandler) UpdateMemberRole(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
memberUserID := c.Param("user_id")
|
|
operatorUserID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
var req types.UpdateMemberRoleRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
if err := h.orgService.UpdateMemberRole(ctx, orgID, memberUserID, req.Role, operatorUserID); err != nil {
|
|
logger.Errorf(ctx, "Failed to update member role: %v", err)
|
|
c.Error(apperrors.NewForbiddenError("Permission denied or invalid operation"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Member role updated successfully",
|
|
})
|
|
}
|
|
|
|
// RemoveMember removes a member from an organization
|
|
// @Summary 移除成员
|
|
// @Description 从组织中移除成员(需要管理员权限)
|
|
// @Tags 组织管理
|
|
// @Param id path string true "组织ID"
|
|
// @Param user_id path string true "用户ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/members/{user_id} [delete]
|
|
func (h *OrganizationHandler) RemoveMember(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
memberUserID := c.Param("user_id")
|
|
operatorUserID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
if err := h.orgService.RemoveMember(ctx, orgID, memberUserID, operatorUserID); err != nil {
|
|
logger.Errorf(ctx, "Failed to remove member: %v", err)
|
|
c.Error(apperrors.NewForbiddenError("Permission denied or invalid operation"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Member removed successfully",
|
|
})
|
|
}
|
|
|
|
// GenerateInviteCode generates a new invite code
|
|
// @Summary 生成邀请码
|
|
// @Description 生成新的组织邀请码(需要管理员权限)
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/invite-code [post]
|
|
func (h *OrganizationHandler) GenerateInviteCode(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
code, err := h.orgService.GenerateInviteCode(ctx, orgID, userID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to generate invite code: %v", err)
|
|
c.Error(apperrors.NewForbiddenError("Permission denied"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"invite_code": code,
|
|
})
|
|
}
|
|
|
|
// PreviewByInviteCode previews organization info by invite code (without joining)
|
|
// @Summary 通过邀请码预览组织
|
|
// @Description 通过邀请码获取组织基本信息(不加入)
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param code path string true "邀请码"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 404 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/preview/{code} [get]
|
|
func (h *OrganizationHandler) PreviewByInviteCode(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
inviteCode := c.Param("code")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
// Get organization by invite code
|
|
org, err := h.orgService.GetOrganizationByInviteCode(ctx, inviteCode)
|
|
if err != nil {
|
|
c.Error(apperrors.NewNotFoundError("Invalid invite code"))
|
|
return
|
|
}
|
|
|
|
// Get member count
|
|
members, _ := h.orgService.ListMembers(ctx, org.ID)
|
|
memberCount := len(members)
|
|
|
|
// Get shared knowledge bases count
|
|
shares, _ := h.shareService.ListSharesByOrganization(ctx, org.ID)
|
|
shareCount := len(shares)
|
|
// Get shared agents count
|
|
agentShares, _ := h.agentShareService.ListSharesByOrganization(ctx, org.ID)
|
|
agentShareCount := len(agentShares)
|
|
|
|
// Check if user is already a member
|
|
_, memberErr := h.orgService.GetMember(ctx, org.ID, userID)
|
|
isAlreadyMember := memberErr == nil
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": gin.H{
|
|
"id": org.ID,
|
|
"name": org.Name,
|
|
"description": org.Description,
|
|
"avatar": org.Avatar,
|
|
"member_count": memberCount,
|
|
"share_count": shareCount,
|
|
"agent_share_count": agentShareCount,
|
|
"is_already_member": isAlreadyMember,
|
|
"require_approval": org.RequireApproval,
|
|
"created_at": org.CreatedAt,
|
|
},
|
|
})
|
|
}
|
|
|
|
// JoinByInviteCode joins an organization by invite code
|
|
// @Summary 通过邀请码加入组织
|
|
// @Description 使用邀请码加入组织
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body types.JoinOrganizationRequest true "邀请码"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 404 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/join [post]
|
|
func (h *OrganizationHandler) JoinByInviteCode(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
var req types.JoinOrganizationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
org, err := h.orgService.JoinByInviteCode(ctx, req.InviteCode, userID, tenantID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to join organization: %v", err)
|
|
if errors.Is(err, service.ErrOrgMemberLimitReached) {
|
|
c.Error(apperrors.NewValidationError("该空间成员已满,无法加入"))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewNotFoundError("Invalid invite code"))
|
|
return
|
|
}
|
|
|
|
logger.Infof(ctx, "User %s joined organization %s", secutils.SanitizeForLog(userID), org.ID)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": h.toOrgResponse(ctx, org, userID),
|
|
})
|
|
}
|
|
|
|
// SubmitJoinRequest submits a join request for organizations that require approval
|
|
// @Summary 提交加入申请
|
|
// @Description 对需要审核的组织提交加入申请
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body types.SubmitJoinRequestRequest true "申请信息"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 400 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/join-request [post]
|
|
func (h *OrganizationHandler) SubmitJoinRequest(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
var req types.SubmitJoinRequestRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Get organization by invite code
|
|
org, err := h.orgService.GetOrganizationByInviteCode(ctx, req.InviteCode)
|
|
if err != nil {
|
|
c.Error(apperrors.NewNotFoundError("Invalid invite code"))
|
|
return
|
|
}
|
|
|
|
// Check if organization requires approval
|
|
if !org.RequireApproval {
|
|
c.Error(apperrors.NewValidationError("This organization does not require approval. Use the join endpoint instead."))
|
|
return
|
|
}
|
|
|
|
// Check if user is already a member
|
|
_, memberErr := h.orgService.GetMember(ctx, org.ID, userID)
|
|
if memberErr == nil {
|
|
c.Error(apperrors.NewValidationError("You are already a member of this organization"))
|
|
return
|
|
}
|
|
|
|
// Validate requested role: only viewer/editor/admin allowed
|
|
requestedRole := req.Role
|
|
if requestedRole != "" && !requestedRole.IsValid() {
|
|
c.Error(apperrors.NewValidationError("Invalid role; must be viewer, editor, or admin"))
|
|
return
|
|
}
|
|
|
|
// Submit join request (service defaults to viewer if role empty)
|
|
request, err := h.orgService.SubmitJoinRequest(ctx, org.ID, userID, tenantID, req.Message, requestedRole)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to submit join request: %v", err)
|
|
if errors.Is(err, service.ErrOrgMemberLimitReached) {
|
|
c.Error(apperrors.NewValidationError("该空间成员已满,无法提交加入申请"))
|
|
return
|
|
}
|
|
if err.Error() == "pending request already exists" {
|
|
c.Error(apperrors.NewValidationError("You have already submitted a request to join this organization"))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewInternalServerError("Failed to submit join request"))
|
|
return
|
|
}
|
|
|
|
logger.Infof(ctx, "User %s submitted join request for organization %s", secutils.SanitizeForLog(userID), org.ID)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": request,
|
|
})
|
|
}
|
|
|
|
// SearchOrganizations returns searchable (discoverable) organizations
|
|
// @Summary 搜索可加入的空间
|
|
// @Description 搜索已开放可被搜索的空间,用于发现并加入
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param q query string false "搜索关键词(空间名称或描述)"
|
|
// @Param limit query int false "返回数量限制" default(20)
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Security Bearer
|
|
// @Router /organizations/search [get]
|
|
func (h *OrganizationHandler) SearchOrganizations(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
query := c.Query("q")
|
|
limit := 20
|
|
if l := c.Query("limit"); l != "" {
|
|
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 100 {
|
|
limit = n
|
|
}
|
|
}
|
|
resp, err := h.orgService.SearchSearchableOrganizations(ctx, userID, query, limit)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to search organizations: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to search organizations"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": resp.Organizations,
|
|
"total": resp.Total,
|
|
})
|
|
}
|
|
|
|
// JoinByOrganizationID joins a searchable organization by ID (no invite code)
|
|
// @Summary 通过空间 ID 加入(可搜索空间)
|
|
// @Description 加入已开放可被搜索的空间,无需邀请码
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body types.JoinByOrganizationIDRequest true "空间 ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/join-by-id [post]
|
|
func (h *OrganizationHandler) JoinByOrganizationID(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
var req types.JoinByOrganizationIDRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
// Validate requested role if provided
|
|
requestedRole := req.Role
|
|
if requestedRole != "" && !requestedRole.IsValid() {
|
|
c.Error(apperrors.NewValidationError("Invalid role; must be viewer, editor, or admin"))
|
|
return
|
|
}
|
|
org, err := h.orgService.JoinByOrganizationID(ctx, req.OrganizationID, userID, tenantID, req.Message, requestedRole)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to join organization by ID: %v", err)
|
|
if errors.Is(err, service.ErrOrgNotFound) {
|
|
c.Error(apperrors.NewNotFoundError("Organization not found or not open for search"))
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrOrgPermissionDenied) {
|
|
c.Error(apperrors.NewForbiddenError("Organization not open for search"))
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrOrgMemberLimitReached) {
|
|
c.Error(apperrors.NewValidationError("该空间成员已满,无法加入"))
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrInvalidRole) {
|
|
c.Error(apperrors.NewValidationError("Invalid role"))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewInternalServerError("Failed to join organization"))
|
|
return
|
|
}
|
|
logger.Infof(ctx, "User %s joined organization %s by ID", secutils.SanitizeForLog(userID), org.ID)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": h.toOrgResponse(ctx, org, userID),
|
|
})
|
|
}
|
|
|
|
// RequestRoleUpgrade submits a request to upgrade role in an organization
|
|
// @Summary 申请权限升级
|
|
// @Description 现有成员申请更高权限
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Param request body types.RequestRoleUpgradeRequest true "申请信息"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 400 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/request-upgrade [post]
|
|
func (h *OrganizationHandler) RequestRoleUpgrade(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
var req types.RequestRoleUpgradeRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Validate requested role
|
|
if !req.RequestedRole.IsValid() {
|
|
c.Error(apperrors.NewValidationError("Invalid role; must be viewer, editor, or admin"))
|
|
return
|
|
}
|
|
|
|
request, err := h.orgService.RequestRoleUpgrade(ctx, orgID, userID, tenantID, req.RequestedRole, req.Message)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to submit role upgrade request: %v", err)
|
|
if err.Error() == "pending request already exists" {
|
|
c.Error(apperrors.NewValidationError("You already have a pending upgrade request"))
|
|
return
|
|
}
|
|
if err.Error() == "user is not a member of this organization" {
|
|
c.Error(apperrors.NewValidationError("You are not a member of this organization"))
|
|
return
|
|
}
|
|
if err.Error() == "user is already an admin" {
|
|
c.Error(apperrors.NewValidationError("You are already an admin"))
|
|
return
|
|
}
|
|
if err.Error() == "cannot request upgrade to same or lower role" {
|
|
c.Error(apperrors.NewValidationError("Cannot request upgrade to same or lower role"))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewInternalServerError("Failed to submit upgrade request"))
|
|
return
|
|
}
|
|
|
|
logger.Infof(ctx, "User %s submitted role upgrade request for organization %s", secutils.SanitizeForLog(userID), orgID)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": request,
|
|
})
|
|
}
|
|
|
|
// LeaveOrganization allows a user to leave an organization
|
|
// @Summary 退出组织
|
|
// @Description 退出指定组织
|
|
// @Tags 组织管理
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/leave [post]
|
|
func (h *OrganizationHandler) LeaveOrganization(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
// Check if user is the owner
|
|
org, err := h.orgService.GetOrganization(ctx, orgID)
|
|
if err != nil {
|
|
c.Error(apperrors.NewNotFoundError("Organization not found"))
|
|
return
|
|
}
|
|
|
|
if org.OwnerID == userID {
|
|
c.Error(apperrors.NewForbiddenError("Organization owner cannot leave. Please transfer ownership or delete the organization."))
|
|
return
|
|
}
|
|
|
|
// Remove the user from the organization
|
|
if err := h.orgService.RemoveMember(ctx, orgID, userID, userID); err != nil {
|
|
logger.Errorf(ctx, "Failed to leave organization: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to leave organization"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Left organization successfully",
|
|
})
|
|
}
|
|
|
|
// ListJoinRequests lists pending join requests for an organization (admin only)
|
|
// @Summary 获取待审核加入申请列表
|
|
// @Description 获取组织的待审核加入申请(仅管理员)
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/join-requests [get]
|
|
func (h *OrganizationHandler) ListJoinRequests(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
// Check admin
|
|
isAdmin, err := h.orgService.IsOrgAdmin(ctx, orgID, userID)
|
|
if err != nil || !isAdmin {
|
|
c.Error(apperrors.NewForbiddenError("Only organization admins can view join requests"))
|
|
return
|
|
}
|
|
|
|
requests, err := h.orgService.ListJoinRequests(ctx, orgID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list join requests: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list join requests"))
|
|
return
|
|
}
|
|
|
|
// Only return pending requests for approval UI
|
|
resp := make([]types.JoinRequestResponse, 0)
|
|
for _, r := range requests {
|
|
if r.Status != types.JoinRequestStatusPending {
|
|
continue
|
|
}
|
|
item := types.JoinRequestResponse{
|
|
ID: r.ID,
|
|
UserID: r.UserID,
|
|
Message: r.Message,
|
|
RequestType: string(r.RequestType),
|
|
PrevRole: string(r.PrevRole),
|
|
RequestedRole: string(r.RequestedRole),
|
|
Status: string(r.Status),
|
|
CreatedAt: r.CreatedAt,
|
|
ReviewedAt: r.ReviewedAt,
|
|
}
|
|
// Default request_type to 'join' for backward compatibility
|
|
if item.RequestType == "" {
|
|
item.RequestType = string(types.JoinRequestTypeJoin)
|
|
}
|
|
if r.User != nil {
|
|
item.Username = r.User.Username
|
|
item.Email = r.User.Email
|
|
}
|
|
resp = append(resp, item)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": types.ListJoinRequestsResponse{
|
|
Requests: resp,
|
|
Total: int64(len(resp)),
|
|
},
|
|
})
|
|
}
|
|
|
|
// ReviewJoinRequest approves or rejects a join request (admin only)
|
|
// @Summary 审核加入申请
|
|
// @Description 通过或拒绝加入申请(仅管理员)
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Param request_id path string true "申请ID"
|
|
// @Param request body types.ReviewJoinRequestRequest true "审核结果"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/join-requests/{request_id}/review [put]
|
|
func (h *OrganizationHandler) ReviewJoinRequest(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
requestID := c.Param("request_id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
// Check admin
|
|
isAdmin, err := h.orgService.IsOrgAdmin(ctx, orgID, userID)
|
|
if err != nil || !isAdmin {
|
|
c.Error(apperrors.NewForbiddenError("Only organization admins can review join requests"))
|
|
return
|
|
}
|
|
|
|
var req types.ReviewJoinRequestRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
var assignRole *types.OrgMemberRole
|
|
if req.Role != "" {
|
|
if !req.Role.IsValid() {
|
|
c.Error(apperrors.NewValidationError("Invalid role; must be viewer, editor, or admin"))
|
|
return
|
|
}
|
|
assignRole = &req.Role
|
|
}
|
|
|
|
if err := h.orgService.ReviewJoinRequest(ctx, orgID, requestID, req.Approved, userID, req.Message, assignRole); err != nil {
|
|
logger.Errorf(ctx, "Failed to review join request: %v", err)
|
|
if errors.Is(err, service.ErrOrgMemberLimitReached) {
|
|
c.Error(apperrors.NewValidationError("空间成员已满,无法通过该加入申请"))
|
|
return
|
|
}
|
|
if err.Error() == "request has already been reviewed" {
|
|
c.Error(apperrors.NewValidationError("Request has already been reviewed"))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewInternalServerError("Failed to review join request"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Review completed",
|
|
})
|
|
}
|
|
|
|
// ShareKnowledgeBase shares a knowledge base to an organization
|
|
// @Summary 共享知识库到组织
|
|
// @Description 将知识库共享到指定组织
|
|
// @Tags 知识库共享
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "知识库ID"
|
|
// @Param request body types.ShareKnowledgeBaseRequest true "共享信息"
|
|
// @Success 201 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledge-bases/{id}/shares [post]
|
|
func (h *OrganizationHandler) ShareKnowledgeBase(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
kbID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
var req types.ShareKnowledgeBaseRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
share, err := h.shareService.ShareKnowledgeBase(ctx, kbID, req.OrganizationID, userID, tenantID, req.Permission)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to share knowledge base: %v", err)
|
|
if errors.Is(err, service.ErrOrgRoleCannotShare) {
|
|
c.Error(apperrors.NewForbiddenError("Only editors and admins can share knowledge bases to this organization"))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewForbiddenError("Permission denied or invalid operation"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"success": true,
|
|
"data": share,
|
|
})
|
|
}
|
|
|
|
// ListKBShares lists all shares for a knowledge base
|
|
// @Summary 获取知识库的共享列表
|
|
// @Description 获取知识库的所有共享记录
|
|
// @Tags 知识库共享
|
|
// @Produce json
|
|
// @Param id path string true "知识库ID"
|
|
// @Success 200 {object} types.ListSharesResponse
|
|
// @Security Bearer
|
|
// @Router /knowledge-bases/{id}/shares [get]
|
|
func (h *OrganizationHandler) ListKBShares(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
kbID := c.Param("id")
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
if tenantID == 0 {
|
|
c.Error(apperrors.NewUnauthorizedError("Unauthorized"))
|
|
return
|
|
}
|
|
|
|
shares, err := h.shareService.ListSharesByKnowledgeBase(ctx, kbID, tenantID)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrKBNotFound) {
|
|
c.Error(apperrors.NewNotFoundError("Knowledge base not found"))
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrNotKBOwner) {
|
|
c.Error(apperrors.NewForbiddenError("Only the knowledge base owner can list its shares"))
|
|
return
|
|
}
|
|
logger.Errorf(ctx, "Failed to list shares: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list shares"))
|
|
return
|
|
}
|
|
|
|
response := make([]types.KnowledgeBaseShareResponse, 0, len(shares))
|
|
for _, s := range shares {
|
|
resp := types.KnowledgeBaseShareResponse{
|
|
ID: s.ID,
|
|
KnowledgeBaseID: s.KnowledgeBaseID,
|
|
OrganizationID: s.OrganizationID,
|
|
SharedByUserID: s.SharedByUserID,
|
|
SourceTenantID: s.SourceTenantID,
|
|
Permission: string(s.Permission),
|
|
CreatedAt: s.CreatedAt,
|
|
}
|
|
if s.Organization != nil {
|
|
resp.OrganizationName = s.Organization.Name
|
|
}
|
|
response = append(response, resp)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": types.ListSharesResponse{
|
|
Shares: response,
|
|
Total: int64(len(response)),
|
|
},
|
|
})
|
|
}
|
|
|
|
// UpdateSharePermission updates the permission of a share
|
|
// @Summary 更新共享权限
|
|
// @Description 更新知识库共享的权限级别
|
|
// @Tags 知识库共享
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "知识库ID"
|
|
// @Param share_id path string true "共享记录ID"
|
|
// @Param request body types.UpdateSharePermissionRequest true "权限信息"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledge-bases/{id}/shares/{share_id} [put]
|
|
func (h *OrganizationHandler) UpdateSharePermission(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
shareID := c.Param("share_id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
var req types.UpdateSharePermissionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
if err := h.shareService.UpdateSharePermission(ctx, shareID, req.Permission, userID); err != nil {
|
|
logger.Errorf(ctx, "Failed to update share permission: %v", err)
|
|
c.Error(apperrors.NewForbiddenError("Permission denied"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Share permission updated successfully",
|
|
})
|
|
}
|
|
|
|
// RemoveShare removes a share
|
|
// @Summary 取消共享
|
|
// @Description 取消知识库的共享
|
|
// @Tags 知识库共享
|
|
// @Param id path string true "知识库ID"
|
|
// @Param share_id path string true "共享记录ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /knowledge-bases/{id}/shares/{share_id} [delete]
|
|
func (h *OrganizationHandler) RemoveShare(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
shareID := c.Param("share_id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
if err := h.shareService.RemoveShare(ctx, shareID, userID); err != nil {
|
|
logger.Errorf(ctx, "Failed to remove share: %v", err)
|
|
c.Error(apperrors.NewForbiddenError("Permission denied"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Share removed successfully",
|
|
})
|
|
}
|
|
|
|
// ListOrgShares lists all knowledge bases shared to a specific organization
|
|
// @Summary 获取组织的共享知识库列表
|
|
// @Description 获取共享到指定组织的所有知识库
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} types.ListSharesResponse
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/shares [get]
|
|
func (h *OrganizationHandler) ListOrgShares(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
// Check if user is a member and get their role for effective-permission calculation
|
|
member, err := h.orgService.GetMember(ctx, orgID, userID)
|
|
if err != nil {
|
|
c.Error(apperrors.NewForbiddenError("You are not a member of this organization"))
|
|
return
|
|
}
|
|
myRoleInOrg := member.Role
|
|
|
|
shares, err := h.shareService.ListSharesByOrganization(ctx, orgID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list organization shares: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list shares"))
|
|
return
|
|
}
|
|
|
|
response := make([]types.KnowledgeBaseShareResponse, 0, len(shares))
|
|
for _, s := range shares {
|
|
// Effective permission for current user = min(share permission, my role in org)
|
|
effectivePerm := s.Permission
|
|
if !myRoleInOrg.HasPermission(s.Permission) {
|
|
effectivePerm = myRoleInOrg
|
|
}
|
|
resp := types.KnowledgeBaseShareResponse{
|
|
ID: s.ID,
|
|
KnowledgeBaseID: s.KnowledgeBaseID,
|
|
OrganizationID: s.OrganizationID,
|
|
SharedByUserID: s.SharedByUserID,
|
|
SourceTenantID: s.SourceTenantID,
|
|
Permission: string(s.Permission),
|
|
MyRoleInOrg: string(myRoleInOrg),
|
|
MyPermission: string(effectivePerm),
|
|
CreatedAt: s.CreatedAt,
|
|
}
|
|
if s.KnowledgeBase != nil {
|
|
resp.KnowledgeBaseName = s.KnowledgeBase.Name
|
|
resp.KnowledgeBaseType = s.KnowledgeBase.Type
|
|
// Get knowledge count for document type
|
|
if count, err := h.knowledgeRepo.CountKnowledgeByKnowledgeBaseID(ctx, s.SourceTenantID, s.KnowledgeBaseID); err == nil {
|
|
resp.KnowledgeCount = count
|
|
}
|
|
// Get chunk count for FAQ type
|
|
if count, err := h.chunkRepo.CountChunksByKnowledgeBaseID(ctx, s.SourceTenantID, s.KnowledgeBaseID); err == nil {
|
|
resp.ChunkCount = count
|
|
}
|
|
}
|
|
// Get shared by user info
|
|
if user, err := h.userService.GetUserByID(ctx, s.SharedByUserID); err == nil && user != nil {
|
|
resp.SharedByUsername = user.Username
|
|
}
|
|
response = append(response, resp)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": types.ListSharesResponse{
|
|
Shares: response,
|
|
Total: int64(len(response)),
|
|
},
|
|
})
|
|
}
|
|
|
|
// ListSharedKnowledgeBases lists all knowledge bases shared to the current user
|
|
// @Summary 获取共享给我的知识库列表
|
|
// @Description 获取通过组织共享给当前用户的所有知识库
|
|
// @Tags 知识库共享
|
|
// @Produce json
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Security Bearer
|
|
// @Router /shared-knowledge-bases [get]
|
|
func (h *OrganizationHandler) ListSharedKnowledgeBases(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := types.MustTenantIDFromContext(ctx)
|
|
|
|
sharedKBs, err := h.shareService.ListSharedKnowledgeBases(ctx, userID, tenantID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list shared knowledge bases: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list shared knowledge bases"))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": sharedKBs,
|
|
"total": len(sharedKBs),
|
|
})
|
|
}
|
|
|
|
// ShareAgent shares an agent to an organization
|
|
func (h *OrganizationHandler) ShareAgent(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
agentID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
var req types.ShareKnowledgeBaseRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
share, err := h.agentShareService.ShareAgent(ctx, agentID, req.OrganizationID, userID, tenantID, req.Permission)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to share agent: %v", err)
|
|
if errors.Is(err, service.ErrOrgRoleCannotShareAgent) {
|
|
c.Error(apperrors.NewForbiddenError("Only editors and admins can share agents to this organization"))
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrAgentNotConfigured) {
|
|
c.Error(apperrors.NewValidationError("Agent is not fully configured. Please set the chat model, and set the rerank model if the knowledge_search tool is enabled in agent settings."))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewForbiddenError("Permission denied or invalid operation"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, gin.H{"success": true, "data": share})
|
|
}
|
|
|
|
// ListAgentShares lists all shares for an agent
|
|
func (h *OrganizationHandler) ListAgentShares(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
agentID := c.Param("id")
|
|
shares, err := h.agentShareService.ListSharesByAgent(ctx, agentID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list agent shares: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list shares"))
|
|
return
|
|
}
|
|
response := make([]types.AgentShareResponse, 0, len(shares))
|
|
for _, s := range shares {
|
|
resp := types.AgentShareResponse{
|
|
ID: s.ID, AgentID: s.AgentID, OrganizationID: s.OrganizationID,
|
|
SharedByUserID: s.SharedByUserID, SourceTenantID: s.SourceTenantID,
|
|
Permission: string(s.Permission), CreatedAt: s.CreatedAt,
|
|
}
|
|
if s.Organization != nil {
|
|
resp.OrganizationName = s.Organization.Name
|
|
}
|
|
response = append(response, resp)
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"shares": response, "total": len(response)}})
|
|
}
|
|
|
|
// RemoveAgentShare removes an agent share
|
|
func (h *OrganizationHandler) RemoveAgentShare(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
shareID := c.Param("share_id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
if err := h.agentShareService.RemoveShare(ctx, shareID, userID); err != nil {
|
|
logger.Errorf(ctx, "Failed to remove agent share: %v", err)
|
|
c.Error(apperrors.NewForbiddenError("Permission denied"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Share removed successfully"})
|
|
}
|
|
|
|
// ListOrgAgentShares lists all agents shared to an organization
|
|
func (h *OrganizationHandler) ListOrgAgentShares(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
member, err := h.orgService.GetMember(ctx, orgID, userID)
|
|
if err != nil {
|
|
c.Error(apperrors.NewForbiddenError("You are not a member of this organization"))
|
|
return
|
|
}
|
|
myRoleInOrg := member.Role
|
|
shares, err := h.agentShareService.ListSharesByOrganization(ctx, orgID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list organization agent shares: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list shares"))
|
|
return
|
|
}
|
|
response := make([]types.AgentShareResponse, 0, len(shares))
|
|
for _, s := range shares {
|
|
effectivePerm := s.Permission
|
|
if !myRoleInOrg.HasPermission(s.Permission) {
|
|
effectivePerm = myRoleInOrg
|
|
}
|
|
resp := types.AgentShareResponse{
|
|
ID: s.ID, AgentID: s.AgentID, OrganizationID: s.OrganizationID,
|
|
SharedByUserID: s.SharedByUserID, SourceTenantID: s.SourceTenantID,
|
|
Permission: string(s.Permission), MyRoleInOrg: string(myRoleInOrg), MyPermission: string(effectivePerm), CreatedAt: s.CreatedAt,
|
|
}
|
|
if s.Agent != nil {
|
|
resp.AgentName = s.Agent.Name
|
|
resp.AgentAvatar = s.Agent.Avatar
|
|
cfg := &s.Agent.Config
|
|
if cfg.KBSelectionMode != "" {
|
|
resp.ScopeKB = cfg.KBSelectionMode
|
|
if cfg.KBSelectionMode == "selected" && len(cfg.KnowledgeBases) > 0 {
|
|
resp.ScopeKBCount = len(cfg.KnowledgeBases)
|
|
}
|
|
} else {
|
|
resp.ScopeKB = "none"
|
|
}
|
|
resp.ScopeWebSearch = cfg.WebSearchEnabled
|
|
if cfg.MCPSelectionMode != "" {
|
|
resp.ScopeMCP = cfg.MCPSelectionMode
|
|
if cfg.MCPSelectionMode == "selected" && len(cfg.MCPServices) > 0 {
|
|
resp.ScopeMCPCount = len(cfg.MCPServices)
|
|
}
|
|
} else {
|
|
resp.ScopeMCP = "none"
|
|
}
|
|
}
|
|
if s.Organization != nil {
|
|
resp.OrganizationName = s.Organization.Name
|
|
}
|
|
if u, err := h.userService.GetUserByID(ctx, s.SharedByUserID); err == nil && u != nil {
|
|
resp.SharedByUsername = u.Username
|
|
}
|
|
response = append(response, resp)
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"shares": response, "total": len(response)}})
|
|
}
|
|
|
|
// ListSharedAgents lists agents shared to the current user
|
|
func (h *OrganizationHandler) ListSharedAgents(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
list, err := h.agentShareService.ListSharedAgents(ctx, userID, tenantID)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to list shared agents: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list shared agents"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
|
|
}
|
|
|
|
// listSpaceKnowledgeBasesInOrganization returns merged list of direct shared KBs and agent-carried KBs in the org (for list and count).
|
|
func (h *OrganizationHandler) listSpaceKnowledgeBasesInOrganization(ctx context.Context, orgID string, userID string, tenantID uint64) ([]*types.OrganizationSharedKnowledgeBaseItem, error) {
|
|
directList, err := h.shareService.ListSharedKnowledgeBasesInOrganization(ctx, orgID, userID, tenantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
directKbIDs := make(map[string]bool)
|
|
for _, item := range directList {
|
|
if item.KnowledgeBase != nil && item.KnowledgeBase.ID != "" {
|
|
directKbIDs[item.KnowledgeBase.ID] = true
|
|
}
|
|
}
|
|
|
|
agentList, err := h.agentShareService.ListSharedAgentsInOrganization(ctx, orgID, userID, tenantID)
|
|
if err != nil {
|
|
return directList, nil
|
|
}
|
|
|
|
orgName := ""
|
|
if len(agentList) > 0 && agentList[0].OrganizationID == orgID {
|
|
orgName = agentList[0].OrgName
|
|
}
|
|
if orgName == "" {
|
|
if org, err := h.orgService.GetOrganization(ctx, orgID); err == nil && org != nil {
|
|
orgName = org.Name
|
|
}
|
|
}
|
|
|
|
merged := make([]*types.OrganizationSharedKnowledgeBaseItem, 0, len(directList)+64)
|
|
merged = append(merged, directList...)
|
|
|
|
for _, agentItem := range agentList {
|
|
if agentItem.Agent == nil {
|
|
continue
|
|
}
|
|
agent := agentItem.Agent
|
|
mode := agent.Config.KBSelectionMode
|
|
if mode == "none" {
|
|
continue
|
|
}
|
|
|
|
var kbIDs []string
|
|
switch mode {
|
|
case "selected":
|
|
if len(agent.Config.KnowledgeBases) == 0 {
|
|
continue
|
|
}
|
|
kbIDs = agent.Config.KnowledgeBases
|
|
case "all":
|
|
kbs, err := h.kbService.ListKnowledgeBasesByTenantID(ctx, agent.TenantID)
|
|
if err != nil {
|
|
logger.Warnf(ctx, "ListKnowledgeBasesByTenantID for agent %s: %v", agent.ID, err)
|
|
continue
|
|
}
|
|
kbIDs = make([]string, 0, len(kbs))
|
|
for _, kb := range kbs {
|
|
if kb != nil && kb.ID != "" {
|
|
kbIDs = append(kbIDs, kb.ID)
|
|
}
|
|
}
|
|
default:
|
|
if len(agent.Config.KnowledgeBases) > 0 {
|
|
kbIDs = agent.Config.KnowledgeBases
|
|
}
|
|
}
|
|
|
|
agentName := agent.Name
|
|
if agentName == "" {
|
|
agentName = agent.ID
|
|
}
|
|
sourceTenantID := agent.TenantID
|
|
|
|
for _, kbID := range kbIDs {
|
|
if kbID == "" || directKbIDs[kbID] {
|
|
continue
|
|
}
|
|
kb, err := h.kbService.GetKnowledgeBaseByIDOnly(ctx, kbID)
|
|
if err != nil || kb == nil {
|
|
continue
|
|
}
|
|
if kb.TenantID != sourceTenantID {
|
|
continue
|
|
}
|
|
directKbIDs[kbID] = true
|
|
|
|
switch kb.Type {
|
|
case types.KnowledgeBaseTypeDocument:
|
|
if count, err := h.knowledgeRepo.CountKnowledgeByKnowledgeBaseID(ctx, sourceTenantID, kb.ID); err == nil {
|
|
kb.KnowledgeCount = count
|
|
}
|
|
case types.KnowledgeBaseTypeFAQ:
|
|
if count, err := h.chunkRepo.CountChunksByKnowledgeBaseID(ctx, sourceTenantID, kb.ID); err == nil {
|
|
kb.ChunkCount = count
|
|
}
|
|
}
|
|
|
|
merged = append(merged, &types.OrganizationSharedKnowledgeBaseItem{
|
|
SharedKnowledgeBaseInfo: types.SharedKnowledgeBaseInfo{
|
|
KnowledgeBase: kb,
|
|
ShareID: "",
|
|
OrganizationID: orgID,
|
|
OrgName: orgName,
|
|
Permission: types.OrgRoleViewer,
|
|
SourceTenantID: sourceTenantID,
|
|
SharedAt: agentItem.SharedAt,
|
|
},
|
|
IsMine: false,
|
|
SourceFromAgent: &types.SourceFromAgentInfo{
|
|
AgentID: agent.ID,
|
|
AgentName: agentName,
|
|
KBSelectionMode: agent.Config.KBSelectionMode,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
return merged, nil
|
|
}
|
|
|
|
// ListOrganizationSharedKnowledgeBases lists all knowledge bases in the given organization (including those shared by the current tenant and those from shared agents), for the list page when a space is selected.
|
|
// @Summary 获取空间内全部知识库(含我共享的、含智能体携带的)
|
|
// @Description 获取指定空间下所有共享知识库,包含直接共享的与通过共享智能体可见的,用于列表页空间视角
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/shared-knowledge-bases [get]
|
|
func (h *OrganizationHandler) ListOrganizationSharedKnowledgeBases(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
list, err := h.listSpaceKnowledgeBasesInOrganization(ctx, orgID, userID, tenantID)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrUserNotInOrg) {
|
|
c.Error(apperrors.NewForbiddenError("You are not a member of this organization"))
|
|
return
|
|
}
|
|
logger.Errorf(ctx, "Failed to list organization shared knowledge bases: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list shared knowledge bases"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
|
|
}
|
|
|
|
// ListOrganizationSharedAgents lists all agents in the given organization (including those shared by the current tenant), for the list page when a space is selected.
|
|
// @Summary 获取空间内全部智能体(含我共享的)
|
|
// @Description 获取指定空间下所有共享智能体,包含他人共享的与我共享的,用于列表页空间视角
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/shared-agents [get]
|
|
func (h *OrganizationHandler) ListOrganizationSharedAgents(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
|
|
list, err := h.agentShareService.ListSharedAgentsInOrganization(ctx, orgID, userID, tenantID)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrUserNotInOrg) {
|
|
c.Error(apperrors.NewForbiddenError("You are not a member of this organization"))
|
|
return
|
|
}
|
|
logger.Errorf(ctx, "Failed to list organization shared agents: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to list shared agents"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
|
|
}
|
|
|
|
// SetSharedAgentDisabledByMeRequest is the body for POST /shared-agents/disabled
|
|
type SetSharedAgentDisabledByMeRequest struct {
|
|
AgentID string `json:"agent_id" binding:"required"`
|
|
Disabled bool `json:"disabled"`
|
|
}
|
|
|
|
// SetSharedAgentDisabledByMe sets whether the current tenant has disabled this shared agent for their conversation dropdown
|
|
func (h *OrganizationHandler) SetSharedAgentDisabledByMe(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
tenantID := c.GetUint64(types.TenantIDContextKey.String())
|
|
uid := userID
|
|
tid := tenantID
|
|
|
|
var req SetSharedAgentDisabledByMeRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewBadRequestError("Invalid request").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
// Derive sourceTenantID: own agent (current tenant) or from shared list
|
|
var sourceTenantID uint64
|
|
agent, err := h.customAgentService.GetAgentByID(ctx, req.AgentID)
|
|
if err == nil && agent != nil && agent.TenantID == tid {
|
|
sourceTenantID = tid
|
|
} else {
|
|
share, err := h.agentShareService.GetShareByAgentIDForUser(ctx, uid, req.AgentID, tid)
|
|
if err != nil || share == nil {
|
|
c.Error(apperrors.NewForbiddenError("No access to this agent"))
|
|
return
|
|
}
|
|
sourceTenantID = share.SourceTenantID
|
|
}
|
|
if err := h.agentShareService.SetSharedAgentDisabledByMe(ctx, tid, req.AgentID, sourceTenantID, req.Disabled); err != nil {
|
|
logger.Errorf(ctx, "SetSharedAgentDisabledByMe failed: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to update preference"))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
// toOrgResponse converts an organization to response format
|
|
func (h *OrganizationHandler) toOrgResponse(ctx context.Context, org *types.Organization, currentUserID string) types.OrganizationResponse {
|
|
resp := types.OrganizationResponse{
|
|
ID: org.ID,
|
|
Name: org.Name,
|
|
Description: org.Description,
|
|
Avatar: org.Avatar,
|
|
OwnerID: org.OwnerID,
|
|
IsOwner: org.OwnerID == currentUserID,
|
|
RequireApproval: org.RequireApproval,
|
|
Searchable: org.Searchable,
|
|
MemberLimit: org.MemberLimit,
|
|
InviteCodeValidityDays: org.InviteCodeValidityDays,
|
|
CreatedAt: org.CreatedAt,
|
|
UpdatedAt: org.UpdatedAt,
|
|
}
|
|
|
|
// Get member count
|
|
if members, err := h.orgService.ListMembers(ctx, org.ID); err == nil {
|
|
resp.MemberCount = len(members)
|
|
}
|
|
|
|
// Get shared knowledge base count for this organization
|
|
if shares, err := h.shareService.ListSharesByOrganization(ctx, org.ID); err == nil {
|
|
resp.ShareCount = len(shares)
|
|
}
|
|
// Get shared agent count for this organization
|
|
if agentShares, err := h.agentShareService.ListSharesByOrganization(ctx, org.ID); err == nil {
|
|
resp.AgentShareCount = len(agentShares)
|
|
}
|
|
|
|
// Get current user's role in this organization
|
|
isAdmin := false
|
|
if role, err := h.orgService.GetUserRoleInOrg(ctx, org.ID, currentUserID); err == nil {
|
|
resp.MyRole = string(role)
|
|
isAdmin = (role == types.OrgRoleAdmin)
|
|
}
|
|
if isAdmin || org.OwnerID == currentUserID {
|
|
resp.InviteCode = org.InviteCode
|
|
resp.InviteCodeExpiresAt = org.InviteCodeExpiresAt
|
|
if n, err := h.orgService.CountPendingJoinRequests(ctx, org.ID); err == nil {
|
|
resp.PendingJoinRequestCount = int(n)
|
|
}
|
|
}
|
|
|
|
// Check if current user has pending upgrade request
|
|
if _, err := h.orgService.GetPendingUpgradeRequest(ctx, org.ID, currentUserID); err == nil {
|
|
resp.HasPendingUpgrade = true
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
// SearchUsersForInvite searches users for inviting to organization
|
|
// @Summary 搜索可邀请的用户
|
|
// @Description 搜索用户(排除已有成员)用于邀请加入组织
|
|
// @Tags 组织管理
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Param q query string true "搜索关键词(用户名或邮箱)"
|
|
// @Param limit query int false "返回数量限制" default(10)
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/search-users [get]
|
|
func (h *OrganizationHandler) SearchUsersForInvite(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
query := c.Query("q")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
// Check admin permission
|
|
isAdmin, err := h.orgService.IsOrgAdmin(ctx, orgID, userID)
|
|
if err != nil || !isAdmin {
|
|
c.Error(apperrors.NewForbiddenError("Only organization admins can invite members"))
|
|
return
|
|
}
|
|
|
|
if query == "" {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": []interface{}{},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get limit from query
|
|
limit := 10
|
|
if l := c.Query("limit"); l != "" {
|
|
if _, err := c.GetQuery("limit"); err {
|
|
limit = 10
|
|
}
|
|
}
|
|
|
|
// Search users
|
|
users, err := h.userService.SearchUsers(ctx, query, limit+20) // fetch more to filter out existing members
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to search users: %v", err)
|
|
c.Error(apperrors.NewInternalServerError("Failed to search users"))
|
|
return
|
|
}
|
|
|
|
// Get existing members
|
|
existingMembers, _ := h.orgService.ListMembers(ctx, orgID)
|
|
existingMemberIDs := make(map[string]bool)
|
|
for _, m := range existingMembers {
|
|
existingMemberIDs[m.UserID] = true
|
|
}
|
|
|
|
// Filter out existing members and build response
|
|
var result []gin.H
|
|
for _, u := range users {
|
|
if existingMemberIDs[u.ID] {
|
|
continue
|
|
}
|
|
result = append(result, gin.H{
|
|
"id": u.ID,
|
|
"username": u.Username,
|
|
"email": u.Email,
|
|
"avatar": u.Avatar,
|
|
})
|
|
if len(result) >= limit {
|
|
break
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": result,
|
|
})
|
|
}
|
|
|
|
// InviteMember directly adds a user to organization
|
|
// @Summary 邀请成员
|
|
// @Description 管理员直接添加用户为组织成员
|
|
// @Tags 组织管理
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "组织ID"
|
|
// @Param request body types.InviteMemberRequest true "邀请信息"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 400 {object} apperrors.AppError
|
|
// @Failure 403 {object} apperrors.AppError
|
|
// @Security Bearer
|
|
// @Router /organizations/{id}/invite [post]
|
|
func (h *OrganizationHandler) InviteMember(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
orgID := c.Param("id")
|
|
userID := c.GetString(types.UserIDContextKey.String())
|
|
|
|
// Check admin permission
|
|
isAdmin, err := h.orgService.IsOrgAdmin(ctx, orgID, userID)
|
|
if err != nil || !isAdmin {
|
|
c.Error(apperrors.NewForbiddenError("Only organization admins can invite members"))
|
|
return
|
|
}
|
|
|
|
var req types.InviteMemberRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.Error(apperrors.NewValidationError("Invalid request parameters").WithDetails(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Validate role
|
|
if !req.Role.IsValid() {
|
|
c.Error(apperrors.NewValidationError("Invalid role; must be viewer, editor, or admin"))
|
|
return
|
|
}
|
|
|
|
// Check if user exists
|
|
invitedUser, err := h.userService.GetUserByID(ctx, req.UserID)
|
|
if err != nil {
|
|
c.Error(apperrors.NewNotFoundError("User not found"))
|
|
return
|
|
}
|
|
|
|
// Check if already a member
|
|
_, memberErr := h.orgService.GetMember(ctx, orgID, req.UserID)
|
|
if memberErr == nil {
|
|
c.Error(apperrors.NewValidationError("User is already a member of this organization"))
|
|
return
|
|
}
|
|
|
|
// Add member
|
|
if err := h.orgService.AddMember(ctx, orgID, req.UserID, invitedUser.TenantID, req.Role); err != nil {
|
|
logger.Errorf(ctx, "Failed to add member: %v", err)
|
|
if errors.Is(err, service.ErrOrgMemberLimitReached) {
|
|
c.Error(apperrors.NewValidationError("该空间成员已满,无法添加新成员"))
|
|
return
|
|
}
|
|
c.Error(apperrors.NewInternalServerError("Failed to add member"))
|
|
return
|
|
}
|
|
|
|
logger.Infof(ctx, "User %s invited user %s to organization %s with role %s",
|
|
secutils.SanitizeForLog(userID),
|
|
secutils.SanitizeForLog(req.UserID),
|
|
orgID,
|
|
req.Role)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Member added successfully",
|
|
})
|
|
}
|