forked from baron/baron-sso
테넌트 목록 조회 cursor기반으로 재구성. 사용자 metadata 미사용 필드 제거
This commit is contained in:
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"baron-sso-backend/internal/bootstrap"
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/pagination"
|
||||
"baron-sso-backend/internal/repository"
|
||||
"baron-sso-backend/internal/service"
|
||||
"baron-sso-backend/internal/utils"
|
||||
@@ -81,10 +82,22 @@ type tenantSummary struct {
|
||||
}
|
||||
|
||||
type tenantListResponse struct {
|
||||
Items []tenantSummary `json:"items"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
Total int64 `json:"total"`
|
||||
Items []tenantSummary `json:"items"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
Total int64 `json:"total"`
|
||||
Cursor string `json:"cursor,omitempty"`
|
||||
NextCursor string `json:"nextCursor,omitempty"`
|
||||
}
|
||||
|
||||
func pageTenantsByCursor(tenants []domain.Tenant, limit int, cursorRaw string) ([]domain.Tenant, string, error) {
|
||||
ordered := append([]domain.Tenant(nil), tenants...)
|
||||
pagination.SortByKeyDesc(ordered, func(tenant domain.Tenant) (time.Time, string) {
|
||||
return tenant.CreatedAt, tenant.ID
|
||||
})
|
||||
return pagination.PageByCursor(ordered, limit, cursorRaw, func(tenant domain.Tenant) (time.Time, string) {
|
||||
return tenant.CreatedAt, tenant.ID
|
||||
})
|
||||
}
|
||||
|
||||
type tenantImportResult struct {
|
||||
@@ -115,43 +128,45 @@ type tenantCSVRecord struct {
|
||||
}
|
||||
|
||||
type orgContextTenant struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ParentID *string `json:"parentId"`
|
||||
Status string `json:"status"`
|
||||
Description string `json:"description"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
Visibility string `json:"visibility"`
|
||||
OrgUnitType string `json:"orgUnitType,omitempty"`
|
||||
Config domain.JSONMap `json:"config,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ParentID *string `json:"parentId"`
|
||||
Status string `json:"status"`
|
||||
Description string `json:"description"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
MemberCount int64 `json:"memberCount"`
|
||||
Visibility string `json:"visibility"`
|
||||
OrgUnitType string `json:"orgUnitType,omitempty"`
|
||||
Config domain.JSONMap `json:"config,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Members []orgContextMember `json:"members"`
|
||||
}
|
||||
|
||||
type orgContextUser struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
TenantIDs []string `json:"tenantIds"`
|
||||
TenantSlugs []string `json:"tenantSlugs"`
|
||||
Department string `json:"department,omitempty"`
|
||||
Grade string `json:"grade,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
JobTitle string `json:"jobTitle,omitempty"`
|
||||
Metadata domain.JSONMap `json:"metadata,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
type orgContextMember struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
Department string `json:"department,omitempty"`
|
||||
Grade string `json:"grade,omitempty"`
|
||||
Position string `json:"position,omitempty"`
|
||||
JobTitle string `json:"jobTitle,omitempty"`
|
||||
IsOwner bool `json:"isOwner"`
|
||||
IsLeader bool `json:"isLeader"`
|
||||
IsPrimary bool `json:"isPrimary"`
|
||||
}
|
||||
|
||||
type orgContextMemberAssignment struct {
|
||||
TenantID string
|
||||
Member orgContextMember
|
||||
}
|
||||
|
||||
type orgContextTreeNode struct {
|
||||
orgContextTenant
|
||||
DirectUserIDs []string `json:"directUserIds"`
|
||||
Children []orgContextTreeNode `json:"children"`
|
||||
Children []orgContextTreeNode `json:"children"`
|
||||
}
|
||||
|
||||
type orgContextScope struct {
|
||||
@@ -165,7 +180,6 @@ type orgContextResponse struct {
|
||||
Scope orgContextScope `json:"scope"`
|
||||
Tree *orgContextTreeNode `json:"tree"`
|
||||
Tenants []orgContextTenant `json:"tenants"`
|
||||
Users []orgContextUser `json:"users"`
|
||||
}
|
||||
|
||||
func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error {
|
||||
@@ -213,6 +227,7 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
limit := c.QueryInt("limit", 50)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
parentId := c.Query("parentId")
|
||||
cursorRaw := strings.TrimSpace(c.Query("cursor"))
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
@@ -224,6 +239,7 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
var tenants []domain.Tenant
|
||||
var total int64
|
||||
var err error
|
||||
nextCursor := ""
|
||||
|
||||
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
role := ""
|
||||
@@ -291,21 +307,48 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
tenants, err = h.filterPrivateTenantsForProfile(c.Context(), tenants, profile)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
|
||||
}
|
||||
|
||||
total = int64(len(tenants))
|
||||
if offset < len(tenants) {
|
||||
if cursorRaw != "" {
|
||||
tenants, nextCursor, err = pageTenantsByCursor(tenants, limit, cursorRaw)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
|
||||
}
|
||||
offset = 0
|
||||
} else if offset < len(tenants) {
|
||||
end := offset + limit
|
||||
if end > len(tenants) {
|
||||
end = len(tenants)
|
||||
}
|
||||
tenants = tenants[offset:end]
|
||||
if total > int64(end) && len(tenants) > 0 {
|
||||
last := tenants[len(tenants)-1]
|
||||
nextCursor = pagination.Encode(last.CreatedAt, last.ID)
|
||||
}
|
||||
} else {
|
||||
tenants = []domain.Tenant{}
|
||||
}
|
||||
} else {
|
||||
// Super Admin case
|
||||
tenants, total, err = h.Service.ListTenants(c.Context(), limit, offset, parentId)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
if cursorRaw != "" && h.DB != nil {
|
||||
tenants, total, nextCursor, err = h.listTenantsByCursor(c.Context(), limit, parentId, cursorRaw)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
|
||||
}
|
||||
offset = 0
|
||||
} else {
|
||||
tenants, total, err = h.Service.ListTenants(c.Context(), limit, offset, parentId)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
if total > int64(offset+len(tenants)) && len(tenants) > 0 {
|
||||
last := tenants[len(tenants)-1]
|
||||
nextCursor = pagination.Encode(last.CreatedAt, last.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +364,52 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
items = append(items, summary)
|
||||
}
|
||||
|
||||
return c.JSON(tenantListResponse{Items: items, Limit: limit, Offset: offset, Total: total})
|
||||
return c.JSON(tenantListResponse{
|
||||
Items: items,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: total,
|
||||
Cursor: cursorRaw,
|
||||
NextCursor: nextCursor,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) listTenantsByCursor(ctx context.Context, limit int, parentID string, cursorRaw string) ([]domain.Tenant, int64, string, error) {
|
||||
cursor, err := pagination.Decode(cursorRaw)
|
||||
if err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
countQuery := h.DB.WithContext(ctx).Model(&domain.Tenant{})
|
||||
pageQuery := h.DB.WithContext(ctx).Model(&domain.Tenant{})
|
||||
if parentID != "" {
|
||||
countQuery = countQuery.Where("parent_id = ?", parentID)
|
||||
pageQuery = pageQuery.Where("parent_id = ?", parentID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := countQuery.Count(&total).Error; err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
pageQuery = pagination.ApplyCreatedAtIDCursor(pageQuery, cursor, "created_at", "id")
|
||||
|
||||
var tenants []domain.Tenant
|
||||
if err := pageQuery.
|
||||
Order("created_at desc, id desc").
|
||||
Limit(limit + 1).
|
||||
Preload("Domains").
|
||||
Find(&tenants).Error; err != nil {
|
||||
return nil, 0, "", err
|
||||
}
|
||||
|
||||
nextCursor := ""
|
||||
if len(tenants) > limit {
|
||||
tenants = tenants[:limit]
|
||||
last := tenants[len(tenants)-1]
|
||||
nextCursor = pagination.Encode(last.CreatedAt, last.ID)
|
||||
}
|
||||
return tenants, total, nextCursor, nil
|
||||
}
|
||||
|
||||
func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
@@ -330,6 +418,11 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
allTenants, err = h.filterPrivateTenantsForProfile(c.Context(), allTenants, profile)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, err.Error())
|
||||
}
|
||||
tenants := filterTenantCSVDescendants(allTenants, parentID)
|
||||
|
||||
var buf bytes.Buffer
|
||||
@@ -923,6 +1016,152 @@ func filterPublicTenants(tenants []domain.Tenant) []domain.Tenant {
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (h *TenantHandler) filterPrivateTenantsForProfile(ctx context.Context, tenants []domain.Tenant, profile *domain.UserProfileResponse) ([]domain.Tenant, error) {
|
||||
if profile != nil && domain.NormalizeRole(profile.Role) == domain.RoleSuperAdmin {
|
||||
return tenants, nil
|
||||
}
|
||||
|
||||
privateRoots := privateTenantRootIDs(tenants)
|
||||
if len(privateRoots) == 0 {
|
||||
return tenants, nil
|
||||
}
|
||||
|
||||
allowedPrivateRoots := make(map[string]bool, len(privateRoots))
|
||||
for _, rootID := range privateRoots {
|
||||
allowed, err := h.canViewPrivateTenant(ctx, profile, rootID, tenants)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if allowed {
|
||||
allowedPrivateRoots[rootID] = true
|
||||
}
|
||||
}
|
||||
|
||||
excludedIDs := make(map[string]bool)
|
||||
for _, rootID := range privateRoots {
|
||||
if !allowedPrivateRoots[rootID] {
|
||||
excludedIDs[rootID] = true
|
||||
}
|
||||
}
|
||||
|
||||
changed := true
|
||||
for changed {
|
||||
changed = false
|
||||
for _, tenant := range tenants {
|
||||
if tenant.ParentID != nil && excludedIDs[*tenant.ParentID] && !excludedIDs[tenant.ID] {
|
||||
excludedIDs[tenant.ID] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filtered := make([]domain.Tenant, 0, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
if !excludedIDs[tenant.ID] {
|
||||
filtered = append(filtered, tenant)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func privateTenantRootIDs(tenants []domain.Tenant) []string {
|
||||
tenantByID := make(map[string]domain.Tenant, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
tenantByID[tenant.ID] = tenant
|
||||
}
|
||||
|
||||
roots := make([]string, 0)
|
||||
for _, tenant := range tenants {
|
||||
if tenantVisibility(tenant.Config) != "private" {
|
||||
continue
|
||||
}
|
||||
if tenant.ParentID != nil {
|
||||
parent, ok := tenantByID[*tenant.ParentID]
|
||||
if ok && tenantVisibility(parent.Config) == "private" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
roots = append(roots, tenant.ID)
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
func (h *TenantHandler) canViewPrivateTenant(ctx context.Context, profile *domain.UserProfileResponse, privateRootID string, tenants []domain.Tenant) (bool, error) {
|
||||
if profile == nil {
|
||||
return false, nil
|
||||
}
|
||||
if profileCanManageTenantOrAncestor(profile, privateRootID, tenants) {
|
||||
return true, nil
|
||||
}
|
||||
if h.Keto == nil || strings.TrimSpace(profile.ID) == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
subject := "User:" + profile.ID
|
||||
for _, relation := range []string{"view_private", "view_private_descendants", "view", "manage"} {
|
||||
allowed, err := h.Keto.CheckPermission(ctx, subject, "Tenant", privateRootID, relation)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("private tenant permission check failed: %w", err)
|
||||
}
|
||||
if allowed {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, ancestorID := range tenantAncestorIDs(privateRootID, tenants) {
|
||||
allowed, err := h.Keto.CheckPermission(ctx, subject, "Tenant", ancestorID, "view_private_descendants")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("private tenant descendant permission check failed: %w", err)
|
||||
}
|
||||
if allowed {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func profileCanManageTenantOrAncestor(profile *domain.UserProfileResponse, tenantID string, tenants []domain.Tenant) bool {
|
||||
manageableIDs := make(map[string]bool, len(profile.ManageableTenants))
|
||||
for _, tenant := range profile.ManageableTenants {
|
||||
if tenant.ID != "" {
|
||||
manageableIDs[tenant.ID] = true
|
||||
}
|
||||
}
|
||||
if len(manageableIDs) == 0 {
|
||||
return false
|
||||
}
|
||||
if manageableIDs[tenantID] {
|
||||
return true
|
||||
}
|
||||
for _, ancestorID := range tenantAncestorIDs(tenantID, tenants) {
|
||||
if manageableIDs[ancestorID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func tenantAncestorIDs(tenantID string, tenants []domain.Tenant) []string {
|
||||
tenantByID := make(map[string]domain.Tenant, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
tenantByID[tenant.ID] = tenant
|
||||
}
|
||||
|
||||
ancestors := make([]string, 0)
|
||||
visited := map[string]bool{}
|
||||
current, ok := tenantByID[tenantID]
|
||||
for ok && current.ParentID != nil && *current.ParentID != "" {
|
||||
parentID := *current.ParentID
|
||||
if visited[parentID] {
|
||||
break
|
||||
}
|
||||
visited[parentID] = true
|
||||
ancestors = append(ancestors, parentID)
|
||||
current, ok = tenantByID[parentID]
|
||||
}
|
||||
return ancestors
|
||||
}
|
||||
|
||||
func normalizeTenantUserSchema(value any) ([]any, error) {
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
@@ -1948,25 +2187,28 @@ func (h *TenantHandler) GetOrgContext(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
includeUsers := !strings.EqualFold(strings.TrimSpace(c.Query("includeUsers")), "false")
|
||||
contextUsers := []orgContextUser{}
|
||||
includeUserIDs := strings.EqualFold(strings.TrimSpace(c.Query("includeUserIds")), "true")
|
||||
membersByTenantID := make(map[string][]orgContextMember)
|
||||
if includeUsers {
|
||||
if h.UserRepo == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "user repository is not configured")
|
||||
}
|
||||
contextUsers, err = h.loadOrgContextUsers(c.Context(), tenantIDs, tenantSlugs, tenantByID, tenantBySlug)
|
||||
membersByTenantID, err = h.loadOrgContextMembers(c.Context(), tenantIDs, tenantSlugs, tenantByID, tenantBySlug, includeUserIDs)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
directUserIDsByTenantID := make(map[string][]string)
|
||||
for _, user := range contextUsers {
|
||||
for _, tenantID := range user.TenantIDs {
|
||||
directUserIDsByTenantID[tenantID] = append(directUserIDsByTenantID[tenantID], user.ID)
|
||||
for i := range contextTenants {
|
||||
members := membersByTenantID[contextTenants[i].ID]
|
||||
if members == nil {
|
||||
members = []orgContextMember{}
|
||||
}
|
||||
contextTenants[i].Members = members
|
||||
tenantByID[contextTenants[i].ID] = contextTenants[i]
|
||||
tenantBySlug[strings.ToLower(contextTenants[i].Slug)] = contextTenants[i]
|
||||
}
|
||||
|
||||
tree := buildOrgContextTree(root.ID, scopedTenants, tenantByID, directUserIDsByTenantID)
|
||||
tree := buildOrgContextTree(root.ID, scopedTenants, tenantByID)
|
||||
return c.JSON(orgContextResponse{
|
||||
SchemaVersion: "baron.org-context.v1",
|
||||
IssuedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
@@ -1976,11 +2218,10 @@ func (h *TenantHandler) GetOrgContext(c *fiber.Ctx) error {
|
||||
},
|
||||
Tree: tree,
|
||||
Tenants: contextTenants,
|
||||
Users: contextUsers,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) loadOrgContextUsers(ctx context.Context, tenantIDs, tenantSlugs []string, tenantByID, tenantBySlug map[string]orgContextTenant) ([]orgContextUser, error) {
|
||||
func (h *TenantHandler) loadOrgContextMembers(ctx context.Context, tenantIDs, tenantSlugs []string, tenantByID, tenantBySlug map[string]orgContextTenant, includeUserIDs bool) (map[string][]orgContextMember, error) {
|
||||
usersByID, err := h.UserRepo.FindByTenantIDs(ctx, tenantIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1989,21 +2230,29 @@ func (h *TenantHandler) loadOrgContextUsers(ctx context.Context, tenantIDs, tena
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersByAppointment, _, err := h.UserRepo.List(ctx, 0, 10000, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
contextUsers := make([]orgContextUser, 0, len(usersByID)+len(usersBySlug))
|
||||
for _, user := range append(usersByID, usersBySlug...) {
|
||||
membersByTenantID := make(map[string][]orgContextMember)
|
||||
users := append(usersByID, usersBySlug...)
|
||||
users = append(users, usersByAppointment...)
|
||||
for _, user := range users {
|
||||
if seen[user.ID] || user.Status != domain.UserStatusActive {
|
||||
continue
|
||||
}
|
||||
mapped, ok := mapOrgContextUser(user, tenantByID, tenantBySlug)
|
||||
if !ok {
|
||||
assignments := mapOrgContextMemberAssignments(user, tenantByID, tenantBySlug, includeUserIDs)
|
||||
if len(assignments) == 0 {
|
||||
continue
|
||||
}
|
||||
seen[user.ID] = true
|
||||
contextUsers = append(contextUsers, mapped)
|
||||
for _, assignment := range assignments {
|
||||
membersByTenantID[assignment.TenantID] = append(membersByTenantID[assignment.TenantID], assignment.Member)
|
||||
}
|
||||
}
|
||||
return contextUsers, nil
|
||||
return membersByTenantID, nil
|
||||
}
|
||||
|
||||
func findOrgContextTenantBySlug(tenants []domain.Tenant, slug string) (domain.Tenant, bool) {
|
||||
@@ -2089,62 +2338,119 @@ func mapOrgContextTenant(tenant domain.Tenant) orgContextTenant {
|
||||
Config: tenant.Config,
|
||||
CreatedAt: tenant.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: tenant.UpdatedAt.Format(time.RFC3339),
|
||||
Members: []orgContextMember{},
|
||||
}
|
||||
}
|
||||
|
||||
func mapOrgContextUser(user domain.User, tenantByID, tenantBySlug map[string]orgContextTenant) (orgContextUser, bool) {
|
||||
matchedTenants := make([]orgContextTenant, 0, 2)
|
||||
func mapOrgContextMemberAssignments(user domain.User, tenantByID, tenantBySlug map[string]orgContextTenant, includeUserIDs bool) []orgContextMemberAssignment {
|
||||
assignments := make([]orgContextMemberAssignment, 0, 2)
|
||||
seenTenants := map[string]bool{}
|
||||
addTenant := func(tenant orgContextTenant, ok bool) {
|
||||
appointments := tenantClaimAppointmentsFromTraits(map[string]any(user.Metadata))
|
||||
|
||||
addTenant := func(tenant orgContextTenant, ok bool, appointment map[string]any) {
|
||||
if !ok || seenTenants[tenant.ID] {
|
||||
return
|
||||
}
|
||||
seenTenants[tenant.ID] = true
|
||||
matchedTenants = append(matchedTenants, tenant)
|
||||
if appointment == nil {
|
||||
appointment = lookupTenantClaimAppointment(appointments, tenant.ID, &domain.Tenant{
|
||||
ID: tenant.ID,
|
||||
Slug: tenant.Slug,
|
||||
})
|
||||
}
|
||||
assignments = append(assignments, orgContextMemberAssignment{
|
||||
TenantID: tenant.ID,
|
||||
Member: mapOrgContextMember(user, appointment, includeUserIDs),
|
||||
})
|
||||
}
|
||||
|
||||
for _, appointment := range appointments {
|
||||
for _, key := range []string{"tenantId", "tenant_id"} {
|
||||
if tenantID := tenantClaimString(appointment, key); tenantID != "" {
|
||||
addTenant(tenantByID[tenantID], tenantByID[tenantID].ID != "", appointment)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"tenantSlug", "tenant_slug"} {
|
||||
if tenantSlug := tenantClaimString(appointment, key); tenantSlug != "" {
|
||||
tenant := tenantBySlug[strings.ToLower(tenantSlug)]
|
||||
addTenant(tenant, tenant.ID != "", appointment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user.TenantID != nil {
|
||||
addTenant(tenantByID[*user.TenantID], tenantByID[*user.TenantID].ID != "")
|
||||
addTenant(tenantByID[*user.TenantID], tenantByID[*user.TenantID].ID != "", nil)
|
||||
}
|
||||
if user.Tenant != nil {
|
||||
addTenant(tenantByID[user.Tenant.ID], tenantByID[user.Tenant.ID].ID != "")
|
||||
addTenant(tenantBySlug[strings.ToLower(user.Tenant.Slug)], tenantBySlug[strings.ToLower(user.Tenant.Slug)].ID != "")
|
||||
addTenant(tenantByID[user.Tenant.ID], tenantByID[user.Tenant.ID].ID != "", nil)
|
||||
tenant := tenantBySlug[strings.ToLower(user.Tenant.Slug)]
|
||||
addTenant(tenant, tenant.ID != "", nil)
|
||||
}
|
||||
if user.CompanyCode != "" {
|
||||
addTenant(tenantBySlug[strings.ToLower(strings.TrimSpace(user.CompanyCode))], tenantBySlug[strings.ToLower(strings.TrimSpace(user.CompanyCode))].ID != "")
|
||||
tenant := tenantBySlug[strings.ToLower(strings.TrimSpace(user.CompanyCode))]
|
||||
addTenant(tenant, tenant.ID != "", nil)
|
||||
}
|
||||
for _, companyCode := range user.CompanyCodes {
|
||||
addTenant(tenantBySlug[strings.ToLower(strings.TrimSpace(companyCode))], tenantBySlug[strings.ToLower(strings.TrimSpace(companyCode))].ID != "")
|
||||
tenant := tenantBySlug[strings.ToLower(strings.TrimSpace(companyCode))]
|
||||
addTenant(tenant, tenant.ID != "", nil)
|
||||
}
|
||||
if len(matchedTenants) == 0 {
|
||||
return orgContextUser{}, false
|
||||
}
|
||||
|
||||
tenantIDs := make([]string, 0, len(matchedTenants))
|
||||
tenantSlugs := make([]string, 0, len(matchedTenants))
|
||||
for _, tenant := range matchedTenants {
|
||||
tenantIDs = append(tenantIDs, tenant.ID)
|
||||
tenantSlugs = append(tenantSlugs, tenant.Slug)
|
||||
}
|
||||
return orgContextUser{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
Name: user.Name,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
TenantIDs: tenantIDs,
|
||||
TenantSlugs: tenantSlugs,
|
||||
Department: user.Department,
|
||||
Grade: user.Grade,
|
||||
Position: user.Position,
|
||||
JobTitle: user.JobTitle,
|
||||
Metadata: user.Metadata,
|
||||
CreatedAt: user.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: user.UpdatedAt.Format(time.RFC3339),
|
||||
}, true
|
||||
return assignments
|
||||
}
|
||||
|
||||
func buildOrgContextTree(rootID string, tenants []domain.Tenant, tenantByID map[string]orgContextTenant, directUserIDsByTenantID map[string][]string) *orgContextTreeNode {
|
||||
func mapOrgContextMember(user domain.User, appointment map[string]any, includeUserIDs bool) orgContextMember {
|
||||
grade := user.Grade
|
||||
position := user.Position
|
||||
jobTitle := user.JobTitle
|
||||
department := user.Department
|
||||
if value := tenantClaimString(appointment, "grade"); value != "" {
|
||||
grade = value
|
||||
}
|
||||
if value := tenantClaimString(appointment, "position"); value != "" {
|
||||
position = value
|
||||
}
|
||||
if value := tenantClaimString(appointment, "jobTitle"); value != "" {
|
||||
jobTitle = value
|
||||
}
|
||||
if value := tenantClaimString(appointment, "job_title"); value != "" {
|
||||
jobTitle = value
|
||||
}
|
||||
if value := tenantClaimString(appointment, "department"); value != "" {
|
||||
department = value
|
||||
}
|
||||
isOwner := false
|
||||
if value, ok := metadataBoolFromMap(appointment, "isOwner", "isManager"); ok {
|
||||
isOwner = value
|
||||
}
|
||||
isLeader := isOwner
|
||||
if value, ok := metadataBoolFromMap(appointment, "lead", "isLead"); ok {
|
||||
isLeader = value
|
||||
}
|
||||
isPrimary := false
|
||||
if value, ok := metadataBoolFromMap(appointment, "representative", "isPrimary", "primary"); ok {
|
||||
isPrimary = value
|
||||
}
|
||||
id := ""
|
||||
phone := ""
|
||||
if includeUserIDs {
|
||||
id = user.ID
|
||||
phone = user.Phone
|
||||
}
|
||||
return orgContextMember{
|
||||
ID: id,
|
||||
Email: user.Email,
|
||||
Name: user.Name,
|
||||
Phone: phone,
|
||||
Department: department,
|
||||
Grade: grade,
|
||||
Position: position,
|
||||
JobTitle: jobTitle,
|
||||
IsOwner: isOwner,
|
||||
IsLeader: isLeader,
|
||||
IsPrimary: isPrimary,
|
||||
}
|
||||
}
|
||||
|
||||
func buildOrgContextTree(rootID string, tenants []domain.Tenant, tenantByID map[string]orgContextTenant) *orgContextTreeNode {
|
||||
childrenByParentID := make(map[string][]domain.Tenant)
|
||||
for _, tenant := range tenants {
|
||||
if tenant.ParentID == nil {
|
||||
@@ -2161,7 +2467,6 @@ func buildOrgContextTree(rootID string, tenants []domain.Tenant, tenantByID map[
|
||||
}
|
||||
node := &orgContextTreeNode{
|
||||
orgContextTenant: tenant,
|
||||
DirectUserIDs: directUserIDsByTenantID[tenantID],
|
||||
Children: []orgContextTreeNode{},
|
||||
}
|
||||
for _, child := range childrenByParentID[tenantID] {
|
||||
|
||||
Reference in New Issue
Block a user