forked from baron/baron-sso
fix(backend): improve LoginID synchronization from custom metadata fields
- Centralize LoginID sync logic in syncLoginID helper - Support namespaced metadata in CreateUser, UpdateUser, and BulkCreateUsers - Ensure UpdateUser and UpdateMe always sync LoginID from configured field even if not in update request - Add phone number normalization consistency for custom LoginIDs - Add unit tests for namespaced metadata LoginID sync
This commit is contained in:
@@ -334,12 +334,8 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
tenantID = tenant.ID
|
||||
|
||||
// Sync custom field to LoginID if configured
|
||||
if loginIDField, ok := tenant.Config["loginIdField"].(string); ok && loginIDField != "" {
|
||||
if val, exists := req.Metadata[loginIDField]; exists {
|
||||
if loginIDStr, ok := val.(string); ok && loginIDStr != "" {
|
||||
attributes["id"] = loginIDStr
|
||||
}
|
||||
}
|
||||
if loginIdField, ok := tenant.Config["loginIdField"].(string); ok && loginIdField != "" {
|
||||
syncLoginID(attributes, req.Metadata, tenantID, loginIdField)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,11 +561,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
|
||||
// Sync LoginID from configured custom field (overrides explicit LoginID)
|
||||
if tItem.LoginIDField != "" {
|
||||
if val, exists := item.Metadata[tItem.LoginIDField]; exists {
|
||||
if loginIDStr, ok := val.(string); ok && loginIDStr != "" {
|
||||
attributes["id"] = loginIDStr
|
||||
}
|
||||
}
|
||||
syncLoginID(attributes, item.Metadata, tItem.ID, tItem.LoginIDField)
|
||||
}
|
||||
|
||||
// Merge metadata
|
||||
@@ -1159,32 +1151,6 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
traits["id"] = *req.LoginID
|
||||
}
|
||||
|
||||
// [LoginID Sync based on Tenant Settings]
|
||||
schemaCompCode := extractTraitString(traits, "companyCode")
|
||||
if req.CompanyCode != nil {
|
||||
schemaCompCode = *req.CompanyCode
|
||||
}
|
||||
if schemaCompCode != "" && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), schemaCompCode); err == nil && tenant != nil {
|
||||
if loginIDField, ok := tenant.Config["loginIdField"].(string); ok && loginIDField != "" {
|
||||
// Search in Metadata (could be flat or namespaced)
|
||||
if val, exists := req.Metadata[loginIDField]; exists {
|
||||
if loginIDStr, ok := val.(string); ok && loginIDStr != "" {
|
||||
traits["id"] = loginIDStr
|
||||
}
|
||||
} else if namespaced, exists := req.Metadata[tenant.ID]; exists {
|
||||
if subMeta, ok := namespaced.(map[string]any); ok {
|
||||
if val, exists := subMeta[loginIDField]; exists {
|
||||
if loginIDStr, ok := val.(string); ok && loginIDStr != "" {
|
||||
traits["id"] = loginIDStr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [Namespaced Metadata Sync]
|
||||
coreTraits := map[string]bool{
|
||||
"email": true, "name": true, "phone_number": true,
|
||||
@@ -1212,6 +1178,17 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// [LoginID Sync based on Tenant Settings]
|
||||
// Perform sync AFTER metadata merge to ensure traits contains current values
|
||||
syncCompCode := extractTraitString(traits, "companyCode")
|
||||
if syncCompCode != "" && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), syncCompCode); err == nil && tenant != nil {
|
||||
if loginIdField, ok := tenant.Config["loginIdField"].(string); ok && loginIdField != "" {
|
||||
syncLoginID(traits, req.Metadata, tenant.ID, loginIdField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state := normalizeKratosState(req.Status)
|
||||
|
||||
slog.Info("[UpdateUser] Calling Kratos UpdateIdentity", "userID", userID, "traits", traits, "state", state)
|
||||
@@ -1510,6 +1487,63 @@ func extractTraitString(traits map[string]interface{}, key string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// syncLoginID ensures that the 'id' trait (used as Kratos identifier) is in sync with the configured custom field.
|
||||
func syncLoginID(traits map[string]interface{}, metadata map[string]any, tenantID string, loginIDField string) {
|
||||
if loginIDField == "" || loginIDField == "id" {
|
||||
return
|
||||
}
|
||||
|
||||
var loginID string
|
||||
|
||||
// 1. Check incoming metadata (flat)
|
||||
if val, ok := metadata[loginIDField].(string); ok && val != "" {
|
||||
loginID = val
|
||||
}
|
||||
|
||||
// 2. Check incoming metadata (namespaced by tenant ID)
|
||||
if loginID == "" && tenantID != "" {
|
||||
if namespaced, ok := metadata[tenantID].(map[string]any); ok {
|
||||
if val, ok := namespaced[loginIDField].(string); ok && val != "" {
|
||||
loginID = val
|
||||
}
|
||||
} else if namespaced, ok := metadata[tenantID].(map[string]interface{}); ok {
|
||||
if val, ok := namespaced[loginIDField].(string); ok && val != "" {
|
||||
loginID = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check merged traits (which includes existing metadata)
|
||||
if loginID == "" {
|
||||
// Existing trait (flat)
|
||||
if val, ok := traits[loginIDField].(string); ok && val != "" {
|
||||
loginID = val
|
||||
} else if tenantID != "" {
|
||||
// Existing trait (namespaced)
|
||||
if namespaced, ok := traits[tenantID].(map[string]interface{}); ok {
|
||||
if val, ok := namespaced[loginIDField].(string); ok && val != "" {
|
||||
loginID = val
|
||||
}
|
||||
} else if namespaced, ok := traits[tenantID].(map[string]any); ok {
|
||||
if val, ok := namespaced[loginIDField].(string); ok && val != "" {
|
||||
loginID = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if loginID != "" {
|
||||
// Normalize if it looks like a phone number to be consistent with other identifiers
|
||||
normalized := normalizePhoneNumber(loginID)
|
||||
if normalized != "" {
|
||||
loginID = normalized
|
||||
}
|
||||
|
||||
slog.Info("Syncing LoginID from custom field", "field", loginIDField, "value", loginID, "tenantID", tenantID)
|
||||
traits["id"] = loginID
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user