forked from baron/baron-sso
네이버 계정 정합성 맞춤
This commit is contained in:
@@ -34,17 +34,16 @@ type OryProviderAPI interface {
|
||||
}
|
||||
|
||||
type UserHandler struct {
|
||||
KratosAdmin service.KratosAdminService
|
||||
OryProvider OryProviderAPI
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
KetoOutboxRepo repository.KetoOutboxRepository
|
||||
UserRepo repository.UserRepository
|
||||
UserProjectionRepo repository.UserProjectionRepository
|
||||
UserGroupRepo repository.UserGroupRepository
|
||||
AuditRepo domain.AuditRepository
|
||||
IdentityCache domain.RedisRepository
|
||||
Worksmobile service.WorksmobileSyncer
|
||||
KratosAdmin service.KratosAdminService
|
||||
OryProvider OryProviderAPI
|
||||
TenantService service.TenantService
|
||||
KetoService service.KetoService
|
||||
KetoOutboxRepo repository.KetoOutboxRepository
|
||||
UserRepo repository.UserRepository
|
||||
UserGroupRepo repository.UserGroupRepository
|
||||
AuditRepo domain.AuditRepository
|
||||
IdentityCache domain.RedisRepository
|
||||
Worksmobile service.WorksmobileSyncer
|
||||
}
|
||||
|
||||
func NewUserHandler(kratosAdmin service.KratosAdminService, oryProvider OryProviderAPI, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository, userGroupRepo repository.UserGroupRepository, auditRepo domain.AuditRepository) *UserHandler {
|
||||
@@ -111,6 +110,16 @@ func userAppointmentSliceFromRaw(raw any) []any {
|
||||
appointments = append(appointments, value)
|
||||
}
|
||||
return appointments
|
||||
case []map[string]string:
|
||||
appointments := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
appointment := make(map[string]any, len(value))
|
||||
for key, item := range value {
|
||||
appointment[key] = item
|
||||
}
|
||||
appointments = append(appointments, appointment)
|
||||
}
|
||||
return appointments
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -323,7 +332,7 @@ func sanitizeUserRepresentativeTenants(ctx context.Context, tenantService servic
|
||||
delete(metadata, "primaryTenantIsOwner")
|
||||
cleared = true
|
||||
}
|
||||
if isNonPublicRepresentativeTenant(ctx, tenantService, normalizeMetadataString(metadata["primaryTenantId"]), normalizeMetadataString(metadata["primaryTenantSlug"])) {
|
||||
if isBlockedRepresentativeTenant(ctx, tenantService, normalizeMetadataString(metadata["primaryTenantId"]), normalizeMetadataString(metadata["primaryTenantSlug"])) {
|
||||
clearMetadataPrimary()
|
||||
}
|
||||
|
||||
@@ -336,7 +345,7 @@ func sanitizeUserRepresentativeTenants(ctx context.Context, tenantService servic
|
||||
if tenantSlug == "" {
|
||||
tenantSlug = normalizeMetadataString(appointment["slug"])
|
||||
}
|
||||
if !isNonPublicRepresentativeTenant(ctx, tenantService, tenantID, tenantSlug) {
|
||||
if !isBlockedRepresentativeTenant(ctx, tenantService, tenantID, tenantSlug) {
|
||||
return
|
||||
}
|
||||
appointment["isPrimary"] = false
|
||||
@@ -359,7 +368,7 @@ func sanitizeUserRepresentativeTenants(ctx context.Context, tenantService servic
|
||||
return cleared, nil
|
||||
}
|
||||
|
||||
func isNonPublicRepresentativeTenant(ctx context.Context, tenantService service.TenantService, tenantID string, tenantSlug string) bool {
|
||||
func isBlockedRepresentativeTenant(ctx context.Context, tenantService service.TenantService, tenantID string, tenantSlug string) bool {
|
||||
var tenant *domain.Tenant
|
||||
var err error
|
||||
if strings.TrimSpace(tenantID) != "" {
|
||||
@@ -371,7 +380,7 @@ func isNonPublicRepresentativeTenant(ctx context.Context, tenantService service.
|
||||
return false
|
||||
}
|
||||
visibility := tenantVisibility(tenant.Config)
|
||||
return visibility == "internal" || visibility == "private"
|
||||
return visibility == "private"
|
||||
}
|
||||
|
||||
func primaryTenantIDFromRequest(primaryTenantID string, metadata map[string]any, appointments []map[string]any) string {
|
||||
@@ -544,9 +553,34 @@ func tenantSlugPointerFromRequest(tenantSlug *string, legacyCompanyCode *string)
|
||||
}
|
||||
|
||||
func identityTenantAccessKeys(traits map[string]any) []string {
|
||||
keys := make([]string, 0, 2)
|
||||
if tenantID := strings.ToLower(strings.TrimSpace(extractTraitString(traits, "tenant_id"))); tenantID != "" {
|
||||
keys = append(keys, tenantID)
|
||||
keys := make([]string, 0, 4)
|
||||
seen := make(map[string]bool)
|
||||
appendKey := func(value string) {
|
||||
key := strings.ToLower(strings.TrimSpace(value))
|
||||
if key == "" || seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
appendKey(extractTraitString(traits, "tenant_id"))
|
||||
appendKey(extractTraitString(traits, "tenantSlug"))
|
||||
|
||||
appointments := userAppointmentSliceFromRaw(traits["additionalAppointments"])
|
||||
if len(appointments) == 0 {
|
||||
if metadata, ok := traits["metadata"].(map[string]any); ok {
|
||||
appointments = userAppointmentSliceFromRaw(metadata["additionalAppointments"])
|
||||
}
|
||||
}
|
||||
for _, raw := range appointments {
|
||||
appointment, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
appendKey(normalizeMetadataString(appointment["tenantId"]))
|
||||
appendKey(normalizeMetadataString(appointment["tenantSlug"]))
|
||||
appendKey(normalizeMetadataString(appointment["slug"]))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -673,6 +707,7 @@ func kratosIdentityCursorKey(identity service.KratosIdentity) (time.Time, string
|
||||
}
|
||||
|
||||
func identityMatchesSearch(identity service.KratosIdentity, searchLower string) bool {
|
||||
searchLower = strings.TrimSpace(searchLower)
|
||||
if searchLower == "" {
|
||||
return true
|
||||
}
|
||||
@@ -685,6 +720,9 @@ func identityMatchesSearch(identity service.KratosIdentity, searchLower string)
|
||||
if strings.Contains(strings.ToLower(extractTraitString(identity.Traits, "name")), searchLower) {
|
||||
return true
|
||||
}
|
||||
if identityEmailLocalPartMatchesSearch(identity, searchLower) {
|
||||
return true
|
||||
}
|
||||
rawTraits, err := json.Marshal(identity.Traits)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -692,6 +730,21 @@ func identityMatchesSearch(identity service.KratosIdentity, searchLower string)
|
||||
return strings.Contains(strings.ToLower(string(rawTraits)), searchLower)
|
||||
}
|
||||
|
||||
func identityEmailLocalPartMatchesSearch(identity service.KratosIdentity, searchLower string) bool {
|
||||
if !strings.Contains(searchLower, "@") {
|
||||
return false
|
||||
}
|
||||
searchLocalPart, err := domain.ExtractNormalizedEmailLocalPart(searchLower)
|
||||
if err != nil || searchLocalPart == "" {
|
||||
return false
|
||||
}
|
||||
identityLocalPart, err := domain.ExtractNormalizedEmailLocalPart(extractTraitString(identity.Traits, "email"))
|
||||
if err != nil || identityLocalPart == "" {
|
||||
return false
|
||||
}
|
||||
return searchLocalPart == identityLocalPart
|
||||
}
|
||||
|
||||
func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
// [New] Get requester profile from middleware
|
||||
var requesterRole string
|
||||
@@ -813,11 +866,11 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
searchLower := strings.ToLower(search)
|
||||
|
||||
for _, identity := range identities {
|
||||
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
|
||||
tenantAccessKeys := identityTenantAccessKeys(identity.Traits)
|
||||
|
||||
// Tenant Admin & Member filtering
|
||||
if requesterRole != domain.RoleSuperAdmin {
|
||||
hasAccess := manageableSlugs[tID]
|
||||
hasAccess := anyTenantKeyAllowed(tenantAccessKeys, manageableSlugs)
|
||||
if !hasAccess {
|
||||
continue
|
||||
}
|
||||
@@ -825,7 +878,11 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
|
||||
// Dedicated tenantSlug filter
|
||||
if tenantSlug != "" {
|
||||
matches := tID == targetTenantID
|
||||
targetKeys := map[string]bool{
|
||||
targetTenantID: true,
|
||||
strings.ToLower(tenantSlug): true,
|
||||
}
|
||||
matches := anyTenantKeyAllowed(tenantAccessKeys, targetKeys)
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
@@ -1195,6 +1252,7 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
|
||||
// [Resolve TenantID and Custom Login IDs before Kratos creation]
|
||||
var tenantID string
|
||||
var resolvedTenant *domain.Tenant
|
||||
primaryAppointments := req.AdditionalAppointments
|
||||
if representativeCleared {
|
||||
primaryAppointments = nil
|
||||
@@ -1205,18 +1263,26 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
if tenant, err := h.TenantService.GetTenant(c.Context(), requestedPrimaryTenantID); err == nil && tenant != nil {
|
||||
tenantID = tenant.ID
|
||||
req.CompanyCode = tenant.Slug
|
||||
resolvedTenant = tenant
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.CompanyCode != "" && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), req.CompanyCode); err == nil && tenant != nil {
|
||||
tenantID = tenant.ID
|
||||
resolvedTenant = tenant
|
||||
}
|
||||
}
|
||||
if err := h.ensureInternalDomainNotAssignedToPersonal(c.Context(), email, tenantID, req.CompanyCode, resolvedTenant); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
if tenantID == "" {
|
||||
if req.CompanyCode != "" || requestedPrimaryTenantID != "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid tenant assignment")
|
||||
}
|
||||
if emailUsesInternalPersonalRestrictedDomain(email) {
|
||||
return errorJSON(c, fiber.StatusBadRequest, internalDomainPersonalPolicyMessage(email))
|
||||
}
|
||||
tenant, err := createPersonalTenantForUser(c.Context(), h.TenantService, email)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "failed to create personal tenant")
|
||||
@@ -1304,21 +1370,13 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
// Sync to local DB (Synchronous for immediate consistency)
|
||||
if err := h.UserRepo.Update(c.Context(), localUser); err != nil {
|
||||
slog.Error("[UserHandler] Failed to sync new user to local DB", "email", localUser.Email, "error", err)
|
||||
markUserProjectionFailed(c.Context(), h.UserProjectionRepo, err)
|
||||
}
|
||||
if h.Worksmobile != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpsertIfInScope(c.Context(), *localUser); err != nil {
|
||||
slog.Warn("[UserHandler] Failed to enqueue Worksmobile user sync", "userID", localUser.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update User Login IDs in local DB
|
||||
for i := range loginIDRecords {
|
||||
loginIDRecords[i].UserID = localUser.ID
|
||||
}
|
||||
if err := h.UserRepo.UpdateUserLoginIDs(c.Context(), localUser.ID, loginIDRecords); err != nil {
|
||||
slog.Error("[UserHandler] Failed to update user login IDs", "userID", localUser.ID, "error", err)
|
||||
markUserProjectionFailed(c.Context(), h.UserProjectionRepo, err)
|
||||
}
|
||||
|
||||
// [Keto] Sync relations via Outbox (Synchronous for accurate counting)
|
||||
@@ -1405,7 +1463,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
results := make([]bulkUserResult, 0, len(req.Users))
|
||||
var hanmacScope *hanmacEmailScope
|
||||
var hanmacLocalParts map[string]bool
|
||||
var hanmacLocalParts map[string]hanmacLocalPartOwner
|
||||
hanmacScopeLoaded := false
|
||||
bulkEmailErrors := validateBulkUserEmailUniqueness(req.Users)
|
||||
|
||||
@@ -1414,6 +1472,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
ID string
|
||||
Slug string
|
||||
Name string
|
||||
Type string
|
||||
ParentID *string
|
||||
Schema []any
|
||||
Groups []domain.UserGroup
|
||||
@@ -1428,6 +1487,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
ID: tenant.ID,
|
||||
Slug: tenant.Slug,
|
||||
Name: tenant.Name,
|
||||
Type: tenant.Type,
|
||||
ParentID: tenant.ParentID,
|
||||
}
|
||||
if s, ok := tenant.Config["userSchema"].([]any); ok {
|
||||
@@ -1558,6 +1618,10 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
continue
|
||||
}
|
||||
tenantSlug = tItem.Slug
|
||||
if isPersonalTenantForInternalDomainPolicy(&domain.Tenant{ID: tItem.ID, Slug: tItem.Slug, Type: tItem.Type}) && emailUsesInternalPersonalRestrictedDomain(email) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: internalDomainPersonalPolicyMessage(email)})
|
||||
continue
|
||||
}
|
||||
} else if tenantSlug != "" {
|
||||
tItem, err = resolveTenantBySlug(tenantSlug)
|
||||
if err != nil {
|
||||
@@ -1565,6 +1629,10 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
continue
|
||||
}
|
||||
tenantSlug = tItem.Slug
|
||||
if isPersonalTenantForInternalDomainPolicy(&domain.Tenant{ID: tItem.ID, Slug: tItem.Slug, Type: tItem.Type}) && emailUsesInternalPersonalRestrictedDomain(email) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: internalDomainPersonalPolicyMessage(email)})
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
for _, domainName := range bulkUserEmailDomainCandidates(item.EmailDomain, email) {
|
||||
if domainTenant, ok := resolveTenantByDomain(domainName); ok {
|
||||
@@ -1574,6 +1642,10 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
if tenantSlug == "" {
|
||||
if emailUsesInternalPersonalRestrictedDomain(email) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: internalDomainPersonalPolicyMessage(email)})
|
||||
continue
|
||||
}
|
||||
tItem, err = createPersonalTenantItem(email)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "failed to create personal tenant"})
|
||||
@@ -1582,6 +1654,10 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
tenantSlug = tItem.Slug
|
||||
}
|
||||
}
|
||||
if isPersonalTenantForInternalDomainPolicy(&domain.Tenant{ID: tItem.ID, Slug: tItem.Slug, Type: tItem.Type}) && emailUsesInternalPersonalRestrictedDomain(email) {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: internalDomainPersonalPolicyMessage(email)})
|
||||
continue
|
||||
}
|
||||
|
||||
// Role-based access check
|
||||
if requester != nil && requester.Role != domain.RoleSuperAdmin {
|
||||
@@ -1678,7 +1754,10 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
userEmail = emailEvaluation.Email
|
||||
if emailEvaluation.LocalPart != "" {
|
||||
hanmacLocalParts[emailEvaluation.LocalPart] = true
|
||||
hanmacLocalParts[emailEvaluation.LocalPart] = hanmacLocalPartOwner{
|
||||
Email: userEmail,
|
||||
Name: item.Name,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, _, err := domain.SplitEmailDomain(email); err != nil {
|
||||
@@ -1878,14 +1957,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
|
||||
if err := h.UserRepo.Update(c.Context(), localUser); err != nil {
|
||||
slog.Error("Failed to sync bulk user to local DB", "email", email, "error", err)
|
||||
markUserProjectionFailed(c.Context(), h.UserProjectionRepo, err)
|
||||
}
|
||||
if h.Worksmobile != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpsertIfInScope(c.Context(), *localUser); err != nil {
|
||||
slog.Warn("Failed to enqueue Worksmobile bulk user sync", "userID", localUser.ID, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update User Login IDs in local DB
|
||||
for i := range loginIDRecords {
|
||||
loginIDRecords[i].UserID = localUser.ID
|
||||
@@ -1903,7 +1975,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
|
||||
if err := h.UserRepo.UpdateUserLoginIDs(c.Context(), localUser.ID, loginIDRecords); err != nil {
|
||||
slog.Error("Failed to update user login IDs in bulk", "userID", localUser.ID, "error", err)
|
||||
markUserProjectionFailed(c.Context(), h.UserProjectionRepo, err)
|
||||
results = append(results, bulkUserResult{
|
||||
Email: userEmail,
|
||||
OriginalEmail: emailEvaluation.OriginalEmail,
|
||||
@@ -2165,6 +2236,8 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
// Pre-fetch tenant cache if tenantSlug is being changed.
|
||||
type tenantCacheItem struct {
|
||||
ID string
|
||||
Slug string
|
||||
Type string
|
||||
Schema []any
|
||||
}
|
||||
tenantCache := make(map[string]tenantCacheItem)
|
||||
@@ -2223,6 +2296,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
if req.CompanyCode != nil {
|
||||
delete(traits, "companyCode")
|
||||
delete(traits, "companyCodes")
|
||||
var targetTenant *domain.Tenant
|
||||
|
||||
if req.IsAddTenant {
|
||||
if h.TenantService == nil {
|
||||
@@ -2234,6 +2308,10 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": "invalid tenant assignment"})
|
||||
continue
|
||||
}
|
||||
if err := h.ensureInternalDomainNotAssignedToPersonal(c.Context(), extractTraitString(traits, "email"), tenant.ID, tenant.Slug, tenant); err != nil {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
|
||||
continue
|
||||
}
|
||||
metadata := mergeUserAddTenantAppointment(traits, nil, tenant)
|
||||
if appointments, ok := metadata["additionalAppointments"]; ok {
|
||||
traits["additionalAppointments"] = appointments
|
||||
@@ -2249,12 +2327,22 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
} else if tItem, exists := tenantCache[*req.CompanyCode]; exists {
|
||||
traits["tenant_id"] = tItem.ID
|
||||
targetTenant = &domain.Tenant{ID: tItem.ID, Slug: tItem.Slug, Type: tItem.Type}
|
||||
} else if h.TenantService != nil {
|
||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode)
|
||||
if err == nil && tenant != nil {
|
||||
tItem.ID = tenant.ID
|
||||
tItem.Slug = tenant.Slug
|
||||
tItem.Type = tenant.Type
|
||||
tenantCache[*req.CompanyCode] = tItem
|
||||
traits["tenant_id"] = tenant.ID
|
||||
targetTenant = tenant
|
||||
}
|
||||
}
|
||||
if !req.IsAddTenant {
|
||||
if err := h.ensureInternalDomainNotAssignedToPersonal(c.Context(), extractTraitString(traits, "email"), extractTraitString(traits, "tenant_id"), *req.CompanyCode, targetTenant); err != nil {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2306,8 +2394,8 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
localUser.JobTitle = *req.JobTitle
|
||||
}
|
||||
|
||||
// Resolve TenantID if changing tenantSlug.
|
||||
if req.CompanyCode != nil && h.TenantService != nil {
|
||||
// Resolve TenantID only for representative tenant changes.
|
||||
if req.CompanyCode != nil && !req.IsAddTenant && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode); err == nil && tenant != nil {
|
||||
localUser.TenantID = &tenant.ID
|
||||
}
|
||||
@@ -2315,7 +2403,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
|
||||
_ = h.UserRepo.Update(c.Context(), localUser)
|
||||
if h.Worksmobile != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpsertIfInScope(c.Context(), *localUser); err != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpdateIfInScope(c.Context(), *localUser); err != nil {
|
||||
slog.Warn("Failed to enqueue Worksmobile bulk user update sync", "userID", localUser.ID, "error", err)
|
||||
}
|
||||
}
|
||||
@@ -2468,6 +2556,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
Status *string `json:"status"`
|
||||
TenantSlug *string `json:"tenantSlug"`
|
||||
CompanyCode *string `json:"companyCode"`
|
||||
IsPrimaryTenant bool `json:"isPrimaryTenant"`
|
||||
IsAddTenant bool `json:"isAddTenant"`
|
||||
IsRemoveTenant bool `json:"isRemoveTenant"`
|
||||
Department *string `json:"department"`
|
||||
@@ -2498,6 +2587,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
req.PrimaryTenantID = ""
|
||||
req.PrimaryTenantName = ""
|
||||
req.PrimaryTenantIsOwner = nil
|
||||
req.IsPrimaryTenant = false
|
||||
req.CompanyCode = nil
|
||||
}
|
||||
}
|
||||
@@ -2592,6 +2682,17 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
if requester == nil || domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: only super admin can change user email")
|
||||
}
|
||||
targetTenantID := extractTraitString(traits, "tenant_id")
|
||||
if targetTenantID == "" {
|
||||
targetTenantID = oldTenantID
|
||||
}
|
||||
targetTenantSlug := ""
|
||||
if req.CompanyCode != nil && !req.IsRemoveTenant {
|
||||
targetTenantSlug = strings.TrimSpace(*req.CompanyCode)
|
||||
}
|
||||
if err := h.ensureHanmacEmailAllowed(c.Context(), nextEmail, targetTenantSlug, targetTenantID, userID); err != nil {
|
||||
return errorJSON(c, fiber.StatusConflict, err.Error())
|
||||
}
|
||||
if h.UserRepo != nil {
|
||||
if existing, err := h.UserRepo.FindByEmail(c.Context(), nextEmail); err == nil && existing != nil && existing.ID != userID {
|
||||
return errorJSON(c, fiber.StatusConflict, "email is already used by another user")
|
||||
@@ -2619,6 +2720,8 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
if req.CompanyCode != nil {
|
||||
code := strings.TrimSpace(*req.CompanyCode)
|
||||
var resolvedTenant *domain.Tenant
|
||||
representativeTenantRequested := req.IsPrimaryTenant || strings.TrimSpace(req.PrimaryTenantID) != ""
|
||||
|
||||
if req.IsRemoveTenant {
|
||||
if h.TenantService != nil && code != "" {
|
||||
@@ -2639,10 +2742,11 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !req.IsAddTenant {
|
||||
} else if !req.IsAddTenant && representativeTenantRequested {
|
||||
if h.TenantService != nil && code != "" {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil {
|
||||
traits["tenant_id"] = tenant.ID
|
||||
resolvedTenant = tenant
|
||||
} else {
|
||||
traits["tenant_id"] = ""
|
||||
}
|
||||
@@ -2652,6 +2756,9 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
if err != nil || tenant == nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid tenant assignment")
|
||||
}
|
||||
if err := h.ensureInternalDomainNotAssignedToPersonal(c.Context(), extractTraitString(traits, "email"), tenant.ID, tenant.Slug, tenant); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
req.Metadata = mergeUserAddTenantAppointment(traits, req.Metadata, tenant)
|
||||
if h.KetoOutboxRepo != nil {
|
||||
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
|
||||
@@ -2663,6 +2770,11 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
if !req.IsRemoveTenant && !req.IsAddTenant && representativeTenantRequested {
|
||||
if err := h.ensureInternalDomainNotAssignedToPersonal(c.Context(), extractTraitString(traits, "email"), extractTraitString(traits, "tenant_id"), code, resolvedTenant); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(traits, "companyCode")
|
||||
delete(traits, "companyCodes")
|
||||
@@ -2719,6 +2831,9 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
traits["secondary_emails"] = subEmails
|
||||
traits["worksmobileAliasEmails"] = subEmails
|
||||
}
|
||||
if err := h.ensureInternalDomainNotAssignedToPersonal(c.Context(), extractTraitString(traits, "email"), extractTraitString(traits, "tenant_id"), "", nil); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
// [LoginID Sync based on Tenant Settings]
|
||||
// Perform sync AFTER metadata merge to ensure traits contains current values
|
||||
@@ -2773,10 +2888,9 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
ctx := context.Background() // Use request context if appropriate, but sync must finish
|
||||
if err := h.UserRepo.Update(ctx, updatedLocalUser); err != nil {
|
||||
slog.Error("[UserHandler] Failed to sync updated user to local DB", "userID", updatedLocalUser.ID, "error", err)
|
||||
markUserProjectionFailed(ctx, h.UserProjectionRepo, err)
|
||||
}
|
||||
if h.Worksmobile != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpsertIfInScope(ctx, *updatedLocalUser); err != nil {
|
||||
if err := h.Worksmobile.EnqueueUserUpdateIfInScope(ctx, *updatedLocalUser); err != nil {
|
||||
slog.Warn("[UserHandler] Failed to enqueue Worksmobile updated user sync", "userID", updatedLocalUser.ID, "error", err)
|
||||
}
|
||||
}
|
||||
@@ -2784,7 +2898,6 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
// Update User Login IDs in local DB
|
||||
if err := h.UserRepo.UpdateUserLoginIDs(ctx, updatedLocalUser.ID, loginIDRecords); err != nil {
|
||||
slog.Error("[UserHandler] Failed to update user login IDs", "userID", updatedLocalUser.ID, "error", err)
|
||||
markUserProjectionFailed(ctx, h.UserProjectionRepo, err)
|
||||
}
|
||||
|
||||
// [Keto Sync] asynchronously as it's less critical for immediate UI count
|
||||
@@ -2963,7 +3076,6 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
if h.UserRepo != nil {
|
||||
if err := h.UserRepo.Delete(context.Background(), userID); err != nil {
|
||||
slog.Error("[UserHandler] Failed to delete local user read-model", "userID", userID, "error", err)
|
||||
markUserProjectionFailed(context.Background(), h.UserProjectionRepo, err)
|
||||
} else {
|
||||
slog.Info("[UserHandler] Successfully deleted local user read-model", "userID", userID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user