1
0
forked from baron/baron-sso

조직도 M2M조회 추가, 자동로그인 보완

This commit is contained in:
2026-05-13 13:44:30 +09:00
parent 72288f1d39
commit 8c2b2f71ef
29 changed files with 2985 additions and 81 deletions

View File

@@ -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 {