forked from baron/baron-sso
조직도 M2M조회 추가, 자동로그인 보완
This commit is contained in:
@@ -114,6 +114,60 @@ type tenantCSVRecord struct {
|
||||
OrgUnitType string
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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 orgContextTreeNode struct {
|
||||
orgContextTenant
|
||||
DirectUserIDs []string `json:"directUserIds"`
|
||||
Children []orgContextTreeNode `json:"children"`
|
||||
}
|
||||
|
||||
type orgContextScope struct {
|
||||
TenantID string `json:"tenantId"`
|
||||
TenantSlug string `json:"tenantSlug"`
|
||||
}
|
||||
|
||||
type orgContextResponse struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
IssuedAt string `json:"issuedAt"`
|
||||
Scope orgContextScope `json:"scope"`
|
||||
Tree *orgContextTreeNode `json:"tree"`
|
||||
Tenants []orgContextTenant `json:"tenants"`
|
||||
Users []orgContextUser `json:"users"`
|
||||
}
|
||||
|
||||
func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
@@ -271,10 +325,12 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
tenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
|
||||
parentID := strings.TrimSpace(c.Query("parentId"))
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
tenants := filterTenantCSVDescendants(allTenants, parentID)
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer := csv.NewWriter(&buf)
|
||||
@@ -286,8 +342,8 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
} else if err := writer.Write([]string{"name", "type", "parent_tenant_slug", "slug", "memo", "email_domain", "visibility", "org_unit_type"}); err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
slugByID := make(map[string]string, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
slugByID := make(map[string]string, len(allTenants))
|
||||
for _, tenant := range allTenants {
|
||||
slugByID[tenant.ID] = tenant.Slug
|
||||
}
|
||||
for _, tenant := range tenants {
|
||||
@@ -343,6 +399,41 @@ func (h *TenantHandler) ExportTenantsCSV(c *fiber.Ctx) error {
|
||||
return c.Send(buf.Bytes())
|
||||
}
|
||||
|
||||
func filterTenantCSVDescendants(tenants []domain.Tenant, parentID string) []domain.Tenant {
|
||||
parentID = strings.TrimSpace(parentID)
|
||||
if parentID == "" {
|
||||
return tenants
|
||||
}
|
||||
|
||||
descendantIDs := map[string]bool{}
|
||||
frontier := map[string]bool{parentID: true}
|
||||
for len(frontier) > 0 {
|
||||
next := map[string]bool{}
|
||||
for _, tenant := range tenants {
|
||||
if tenant.ParentID == nil {
|
||||
continue
|
||||
}
|
||||
if !frontier[strings.TrimSpace(*tenant.ParentID)] {
|
||||
continue
|
||||
}
|
||||
if descendantIDs[tenant.ID] {
|
||||
continue
|
||||
}
|
||||
descendantIDs[tenant.ID] = true
|
||||
next[tenant.ID] = true
|
||||
}
|
||||
frontier = next
|
||||
}
|
||||
|
||||
filtered := make([]domain.Tenant, 0, len(descendantIDs))
|
||||
for _, tenant := range tenants {
|
||||
if descendantIDs[tenant.ID] {
|
||||
filtered = append(filtered, tenant)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (h *TenantHandler) ImportTenantsCSV(c *fiber.Ctx) error {
|
||||
reader, err := tenantCSVReaderFromRequest(c)
|
||||
if err != nil {
|
||||
@@ -1818,6 +1909,272 @@ func mapTenantSummary(t domain.Tenant) tenantSummary {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TenantHandler) GetOrgContext(c *fiber.Ctx) error {
|
||||
if c.Locals("apiKeyName") == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "api key authentication is required")
|
||||
}
|
||||
if h.Service == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "tenant service is not configured")
|
||||
}
|
||||
|
||||
allTenants, _, err := h.Service.ListTenants(c.Context(), 10000, 0, "")
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
rootSlug := strings.TrimSpace(c.Query("tenantSlug"))
|
||||
if rootSlug == "" {
|
||||
rootSlug = "hanmac-family"
|
||||
}
|
||||
|
||||
root, ok := findOrgContextTenantBySlug(allTenants, rootSlug)
|
||||
if !ok {
|
||||
return errorJSON(c, fiber.StatusNotFound, "tenant slug not found")
|
||||
}
|
||||
|
||||
scopedTenants := filterOrgContextSubtree(allTenants, root.ID)
|
||||
contextTenants := make([]orgContextTenant, 0, len(scopedTenants))
|
||||
tenantIDs := make([]string, 0, len(scopedTenants))
|
||||
tenantSlugs := make([]string, 0, len(scopedTenants))
|
||||
tenantByID := make(map[string]orgContextTenant, len(scopedTenants))
|
||||
tenantBySlug := make(map[string]orgContextTenant, len(scopedTenants))
|
||||
for _, tenant := range scopedTenants {
|
||||
summary := mapOrgContextTenant(tenant)
|
||||
contextTenants = append(contextTenants, summary)
|
||||
tenantIDs = append(tenantIDs, tenant.ID)
|
||||
tenantSlugs = append(tenantSlugs, tenant.Slug)
|
||||
tenantByID[tenant.ID] = summary
|
||||
tenantBySlug[strings.ToLower(tenant.Slug)] = summary
|
||||
}
|
||||
|
||||
includeUsers := !strings.EqualFold(strings.TrimSpace(c.Query("includeUsers")), "false")
|
||||
contextUsers := []orgContextUser{}
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
tree := buildOrgContextTree(root.ID, scopedTenants, tenantByID, directUserIDsByTenantID)
|
||||
return c.JSON(orgContextResponse{
|
||||
SchemaVersion: "baron.org-context.v1",
|
||||
IssuedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Scope: orgContextScope{
|
||||
TenantID: root.ID,
|
||||
TenantSlug: root.Slug,
|
||||
},
|
||||
Tree: tree,
|
||||
Tenants: contextTenants,
|
||||
Users: contextUsers,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *TenantHandler) loadOrgContextUsers(ctx context.Context, tenantIDs, tenantSlugs []string, tenantByID, tenantBySlug map[string]orgContextTenant) ([]orgContextUser, error) {
|
||||
usersByID, err := h.UserRepo.FindByTenantIDs(ctx, tenantIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersBySlug, err := h.UserRepo.FindByCompanyCodes(ctx, tenantSlugs)
|
||||
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...) {
|
||||
if seen[user.ID] || user.Status != domain.UserStatusActive {
|
||||
continue
|
||||
}
|
||||
mapped, ok := mapOrgContextUser(user, tenantByID, tenantBySlug)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
seen[user.ID] = true
|
||||
contextUsers = append(contextUsers, mapped)
|
||||
}
|
||||
return contextUsers, nil
|
||||
}
|
||||
|
||||
func findOrgContextTenantBySlug(tenants []domain.Tenant, slug string) (domain.Tenant, bool) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(slug))
|
||||
for _, tenant := range tenants {
|
||||
if strings.ToLower(tenant.Slug) == normalized && isOrgContextTenantType(tenant) {
|
||||
return tenant, true
|
||||
}
|
||||
}
|
||||
return domain.Tenant{}, false
|
||||
}
|
||||
|
||||
func isOrgContextTenantType(tenant domain.Tenant) bool {
|
||||
switch strings.ToUpper(tenant.Type) {
|
||||
case domain.TenantTypeCompanyGroup, domain.TenantTypeCompany, domain.TenantTypeOrganization, domain.TenantTypeUserGroup:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func filterOrgContextSubtree(tenants []domain.Tenant, rootID string) []domain.Tenant {
|
||||
descendantIDs := map[string]bool{rootID: true}
|
||||
frontier := map[string]bool{rootID: true}
|
||||
for len(frontier) > 0 {
|
||||
next := map[string]bool{}
|
||||
for _, tenant := range tenants {
|
||||
if tenant.ParentID == nil || !frontier[*tenant.ParentID] || descendantIDs[tenant.ID] {
|
||||
continue
|
||||
}
|
||||
descendantIDs[tenant.ID] = true
|
||||
next[tenant.ID] = true
|
||||
}
|
||||
frontier = next
|
||||
}
|
||||
|
||||
excludedIDs := map[string]bool{}
|
||||
for _, tenant := range tenants {
|
||||
if descendantIDs[tenant.ID] && tenantVisibility(tenant.Config) == "private" {
|
||||
excludedIDs[tenant.ID] = true
|
||||
}
|
||||
}
|
||||
changed := true
|
||||
for changed {
|
||||
changed = false
|
||||
for _, tenant := range tenants {
|
||||
if tenant.ParentID == nil || !descendantIDs[tenant.ID] || excludedIDs[tenant.ID] {
|
||||
continue
|
||||
}
|
||||
if excludedIDs[*tenant.ParentID] {
|
||||
excludedIDs[tenant.ID] = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filtered := make([]domain.Tenant, 0, len(descendantIDs))
|
||||
for _, tenant := range tenants {
|
||||
if descendantIDs[tenant.ID] && !excludedIDs[tenant.ID] && isOrgContextTenantType(tenant) {
|
||||
filtered = append(filtered, tenant)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func mapOrgContextTenant(tenant domain.Tenant) orgContextTenant {
|
||||
domains := make([]string, 0, len(tenant.Domains))
|
||||
for _, domain := range tenant.Domains {
|
||||
domains = append(domains, domain.Domain)
|
||||
}
|
||||
visibility, orgUnitType := tenantCSVOrgConfigValues(tenant.Config)
|
||||
return orgContextTenant{
|
||||
ID: tenant.ID,
|
||||
Type: tenant.Type,
|
||||
Name: tenant.Name,
|
||||
Slug: tenant.Slug,
|
||||
ParentID: tenant.ParentID,
|
||||
Status: tenant.Status,
|
||||
Description: tenant.Description,
|
||||
Domains: domains,
|
||||
Visibility: visibility,
|
||||
OrgUnitType: orgUnitType,
|
||||
Config: tenant.Config,
|
||||
CreatedAt: tenant.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: tenant.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func mapOrgContextUser(user domain.User, tenantByID, tenantBySlug map[string]orgContextTenant) (orgContextUser, bool) {
|
||||
matchedTenants := make([]orgContextTenant, 0, 2)
|
||||
seenTenants := map[string]bool{}
|
||||
addTenant := func(tenant orgContextTenant, ok bool) {
|
||||
if !ok || seenTenants[tenant.ID] {
|
||||
return
|
||||
}
|
||||
seenTenants[tenant.ID] = true
|
||||
matchedTenants = append(matchedTenants, tenant)
|
||||
}
|
||||
|
||||
if user.TenantID != nil {
|
||||
addTenant(tenantByID[*user.TenantID], tenantByID[*user.TenantID].ID != "")
|
||||
}
|
||||
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 != "")
|
||||
}
|
||||
if user.CompanyCode != "" {
|
||||
addTenant(tenantBySlug[strings.ToLower(strings.TrimSpace(user.CompanyCode))], tenantBySlug[strings.ToLower(strings.TrimSpace(user.CompanyCode))].ID != "")
|
||||
}
|
||||
for _, companyCode := range user.CompanyCodes {
|
||||
addTenant(tenantBySlug[strings.ToLower(strings.TrimSpace(companyCode))], tenantBySlug[strings.ToLower(strings.TrimSpace(companyCode))].ID != "")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func buildOrgContextTree(rootID string, tenants []domain.Tenant, tenantByID map[string]orgContextTenant, directUserIDsByTenantID map[string][]string) *orgContextTreeNode {
|
||||
childrenByParentID := make(map[string][]domain.Tenant)
|
||||
for _, tenant := range tenants {
|
||||
if tenant.ParentID == nil {
|
||||
continue
|
||||
}
|
||||
childrenByParentID[*tenant.ParentID] = append(childrenByParentID[*tenant.ParentID], tenant)
|
||||
}
|
||||
|
||||
var build func(tenantID string) *orgContextTreeNode
|
||||
build = func(tenantID string) *orgContextTreeNode {
|
||||
tenant, ok := tenantByID[tenantID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
node := &orgContextTreeNode{
|
||||
orgContextTenant: tenant,
|
||||
DirectUserIDs: directUserIDsByTenantID[tenantID],
|
||||
Children: []orgContextTreeNode{},
|
||||
}
|
||||
for _, child := range childrenByParentID[tenantID] {
|
||||
childNode := build(child.ID)
|
||||
if childNode != nil {
|
||||
node.Children = append(node.Children, *childNode)
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
return build(rootID)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) countTenantMembersFromProjection(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
|
||||
counts := make(map[string]int64, len(tenants))
|
||||
for _, tenant := range tenants {
|
||||
|
||||
Reference in New Issue
Block a user