forked from baron/baron-sso
동기화 기초구조 마련
This commit is contained in:
@@ -118,6 +118,47 @@ func primaryTenantIDFromRequest(primaryTenantID string, metadata map[string]any,
|
||||
return ""
|
||||
}
|
||||
|
||||
func bulkUserEmailDomainCandidates(emailDomain string, email string) []string {
|
||||
values := make([]string, 0, 2)
|
||||
seen := map[string]bool{}
|
||||
add := func(value string) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
if normalized == "" || seen[normalized] {
|
||||
return
|
||||
}
|
||||
seen[normalized] = true
|
||||
values = append(values, normalized)
|
||||
}
|
||||
for _, value := range strings.FieldsFunc(emailDomain, func(r rune) bool {
|
||||
return r == ',' || r == ';' || r == '\n' || r == '\r'
|
||||
}) {
|
||||
add(value)
|
||||
}
|
||||
if _, domainPart, err := domain.SplitEmailDomain(email); err == nil {
|
||||
add(domainPart)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func bulkUserAssignmentContainsTenant(appointments []any, primaryTenantID string, tenantID string) bool {
|
||||
if strings.TrimSpace(tenantID) == "" {
|
||||
return true
|
||||
}
|
||||
if primaryTenantID != "" && primaryTenantID == tenantID {
|
||||
return true
|
||||
}
|
||||
for _, item := range appointments {
|
||||
appointment, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if normalizeMetadataString(appointment["tenantId"]) == tenantID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func metadataBoolFromMap(metadata map[string]any, keys ...string) (bool, bool) {
|
||||
for _, key := range keys {
|
||||
value, ok := metadata[key]
|
||||
@@ -664,17 +705,20 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
type bulkUserItem struct {
|
||||
Email string `json:"email"`
|
||||
LoginID string `json:"loginId"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
TenantSlug string `json:"tenantSlug"`
|
||||
Department string `json:"department"`
|
||||
Grade string `json:"grade"`
|
||||
Position string `json:"position"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Email string `json:"email"`
|
||||
LoginID string `json:"loginId"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
TenantID string `json:"tenantId"`
|
||||
TenantSlug string `json:"tenantSlug"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
Department string `json:"department"`
|
||||
Grade string `json:"grade"`
|
||||
Position string `json:"position"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
AdditionalAppointments []map[string]any `json:"additionalAppointments"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type bulkUserResult struct {
|
||||
@@ -720,15 +764,103 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
// Pre-fetch tenant data to avoid redundant DB calls
|
||||
type tenantCacheItem struct {
|
||||
ID string
|
||||
Slug string
|
||||
Name string
|
||||
Schema []interface{}
|
||||
Groups []domain.UserGroup
|
||||
LoginIDField string
|
||||
}
|
||||
tenantCache := make(map[string]tenantCacheItem)
|
||||
tenantCacheByID := make(map[string]tenantCacheItem)
|
||||
tenantCacheByDomain := make(map[string]tenantCacheItem)
|
||||
|
||||
buildTenantCacheItem := func(tenant *domain.Tenant) tenantCacheItem {
|
||||
tItem := tenantCacheItem{
|
||||
ID: tenant.ID,
|
||||
Slug: tenant.Slug,
|
||||
Name: tenant.Name,
|
||||
}
|
||||
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||
tItem.Schema = s
|
||||
}
|
||||
if lf, ok := tenant.Config["loginIdField"].(string); ok {
|
||||
tItem.LoginIDField = lf
|
||||
}
|
||||
if h.UserGroupRepo != nil {
|
||||
if groups, err := h.UserGroupRepo.ListByTenantID(c.Context(), tenant.ID); err == nil {
|
||||
tItem.Groups = groups
|
||||
}
|
||||
}
|
||||
return tItem
|
||||
}
|
||||
|
||||
cacheTenantItem := func(tItem tenantCacheItem) tenantCacheItem {
|
||||
if tItem.Slug != "" {
|
||||
tenantCache[strings.ToLower(strings.TrimSpace(tItem.Slug))] = tItem
|
||||
}
|
||||
if tItem.ID != "" {
|
||||
tenantCacheByID[tItem.ID] = tItem
|
||||
}
|
||||
return tItem
|
||||
}
|
||||
|
||||
resolveTenantBySlug := func(slug string) (tenantCacheItem, error) {
|
||||
normalizedSlug := strings.ToLower(strings.TrimSpace(slug))
|
||||
if normalizedSlug == "" {
|
||||
return tenantCacheItem{}, errors.New("tenantSlug is required")
|
||||
}
|
||||
if tItem, exists := tenantCache[normalizedSlug]; exists {
|
||||
return tItem, nil
|
||||
}
|
||||
if h.TenantService == nil {
|
||||
return tenantCacheItem{}, errors.New("tenant service unavailable")
|
||||
}
|
||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), normalizedSlug)
|
||||
if err != nil || tenant == nil {
|
||||
return tenantCacheItem{}, errors.New("invalid tenantSlug: tenant not found")
|
||||
}
|
||||
return cacheTenantItem(buildTenantCacheItem(tenant)), nil
|
||||
}
|
||||
|
||||
resolveTenantByID := func(tenantID string) (tenantCacheItem, error) {
|
||||
normalizedID := strings.TrimSpace(tenantID)
|
||||
if normalizedID == "" {
|
||||
return tenantCacheItem{}, errors.New("tenantId is required")
|
||||
}
|
||||
if tItem, exists := tenantCacheByID[normalizedID]; exists {
|
||||
return tItem, nil
|
||||
}
|
||||
if h.TenantService == nil {
|
||||
return tenantCacheItem{}, errors.New("tenant service unavailable")
|
||||
}
|
||||
tenant, err := h.TenantService.GetTenant(c.Context(), normalizedID)
|
||||
if err != nil || tenant == nil {
|
||||
return tenantCacheItem{}, errors.New("invalid tenantId: tenant not found")
|
||||
}
|
||||
return cacheTenantItem(buildTenantCacheItem(tenant)), nil
|
||||
}
|
||||
|
||||
resolveTenantByDomain := func(domainName string) (tenantCacheItem, bool) {
|
||||
normalizedDomain := strings.ToLower(strings.TrimSpace(domainName))
|
||||
if normalizedDomain == "" || h.TenantService == nil {
|
||||
return tenantCacheItem{}, false
|
||||
}
|
||||
if tItem, exists := tenantCacheByDomain[normalizedDomain]; exists {
|
||||
return tItem, true
|
||||
}
|
||||
tenant, err := h.TenantService.GetTenantByDomain(c.Context(), normalizedDomain)
|
||||
if err != nil || tenant == nil {
|
||||
return tenantCacheItem{}, false
|
||||
}
|
||||
tItem := cacheTenantItem(buildTenantCacheItem(tenant))
|
||||
tenantCacheByDomain[normalizedDomain] = tItem
|
||||
return tItem, true
|
||||
}
|
||||
|
||||
for _, item := range req.Users {
|
||||
email := strings.TrimSpace(item.Email)
|
||||
name := strings.TrimSpace(item.Name)
|
||||
tenantID := strings.TrimSpace(item.TenantID)
|
||||
tenantSlug := strings.TrimSpace(item.TenantSlug)
|
||||
dept := strings.TrimSpace(item.Department)
|
||||
|
||||
@@ -737,9 +869,38 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if tenantSlug == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenantSlug is required"})
|
||||
continue
|
||||
var tItem tenantCacheItem
|
||||
var err error
|
||||
if tenantID != "" {
|
||||
tItem, err = resolveTenantByID(tenantID)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
|
||||
continue
|
||||
}
|
||||
if tenantSlug != "" && !strings.EqualFold(tenantSlug, tItem.Slug) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenantId and tenantSlug do not match"})
|
||||
continue
|
||||
}
|
||||
tenantSlug = tItem.Slug
|
||||
} else if tenantSlug != "" {
|
||||
tItem, err = resolveTenantBySlug(tenantSlug)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
|
||||
continue
|
||||
}
|
||||
tenantSlug = tItem.Slug
|
||||
} else {
|
||||
for _, domainName := range bulkUserEmailDomainCandidates(item.EmailDomain, email) {
|
||||
if domainTenant, ok := resolveTenantByDomain(domainName); ok {
|
||||
tItem = domainTenant
|
||||
tenantSlug = domainTenant.Slug
|
||||
break
|
||||
}
|
||||
}
|
||||
if tenantSlug == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenant assignment is required"})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Role-based access check
|
||||
@@ -750,33 +911,47 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Tenant Existence and Resolve ID (with Cache)
|
||||
var tItem tenantCacheItem
|
||||
var exists bool
|
||||
if tItem, exists = tenantCache[tenantSlug]; !exists {
|
||||
if h.TenantService != nil {
|
||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug)
|
||||
if err != nil || tenant == nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "invalid tenantSlug: tenant not found"})
|
||||
resolvedAppointments := make([]any, 0, len(item.AdditionalAppointments)+2)
|
||||
if len(item.AdditionalAppointments) > 0 {
|
||||
appointmentFailed := false
|
||||
for _, rawAppointment := range item.AdditionalAppointments {
|
||||
appointmentTenantSlug := strings.TrimSpace(normalizeMetadataString(rawAppointment["tenantSlug"]))
|
||||
if appointmentTenantSlug == "" {
|
||||
continue
|
||||
}
|
||||
tItem.ID = tenant.ID
|
||||
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||
tItem.Schema = s
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin && appointmentTenantSlug != requester.CompanyCode {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||
appointmentFailed = true
|
||||
break
|
||||
}
|
||||
if lf, ok := tenant.Config["loginIdField"].(string); ok {
|
||||
tItem.LoginIDField = lf
|
||||
}
|
||||
// [Fix] Cache user groups for this tenant to match department
|
||||
if h.UserGroupRepo != nil {
|
||||
if groups, err := h.UserGroupRepo.ListByTenantID(c.Context(), tenant.ID); err == nil {
|
||||
tItem.Groups = groups
|
||||
|
||||
appointmentTenant, exists := tenantCache[strings.ToLower(appointmentTenantSlug)]
|
||||
if !exists {
|
||||
appointmentTenant, err = resolveTenantBySlug(appointmentTenantSlug)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: strings.Replace(err.Error(), "tenantSlug", "additional tenantSlug", 1)})
|
||||
appointmentFailed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tenantCache[tenantSlug] = tItem
|
||||
} else {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenant service unavailable"})
|
||||
appointment := make(map[string]any, len(rawAppointment)+3)
|
||||
for key, value := range rawAppointment {
|
||||
if key == "tenantSlug" || key == "tenantId" || key == "tenantName" {
|
||||
continue
|
||||
}
|
||||
appointment[key] = value
|
||||
}
|
||||
appointment["tenantId"] = appointmentTenant.ID
|
||||
appointment["tenantSlug"] = appointmentTenant.Slug
|
||||
if name := strings.TrimSpace(normalizeMetadataString(rawAppointment["tenantName"])); name != "" {
|
||||
appointment["tenantName"] = name
|
||||
} else {
|
||||
appointment["tenantName"] = appointmentTenant.Name
|
||||
}
|
||||
resolvedAppointments = append(resolvedAppointments, appointment)
|
||||
}
|
||||
if appointmentFailed {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -836,6 +1011,26 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, domainName := range bulkUserEmailDomainCandidates(item.EmailDomain, userEmail) {
|
||||
domainTenant, ok := resolveTenantByDomain(domainName)
|
||||
if !ok || bulkUserAssignmentContainsTenant(resolvedAppointments, tItem.ID, domainTenant.ID) {
|
||||
continue
|
||||
}
|
||||
resolvedAppointments = append(resolvedAppointments, map[string]any{
|
||||
"tenantId": domainTenant.ID,
|
||||
"tenantSlug": domainTenant.Slug,
|
||||
"tenantName": domainTenant.Name,
|
||||
"assignmentSource": "email_domain",
|
||||
"sourceDomain": strings.ToLower(strings.TrimSpace(domainName)),
|
||||
})
|
||||
}
|
||||
if len(resolvedAppointments) > 0 {
|
||||
if item.Metadata == nil {
|
||||
item.Metadata = map[string]any{}
|
||||
}
|
||||
item.Metadata["additionalAppointments"] = resolvedAppointments
|
||||
}
|
||||
|
||||
password, _ := utils.GeneratePasswordWithPolicy(policy)
|
||||
role := item.Role
|
||||
if role == "" {
|
||||
|
||||
Reference in New Issue
Block a user