forked from baron/baron-sso
feat: 커스텀 필드 기반 로그인 ID 연동 기능 추가 (#440)
- Kratos Identity 스키마에 로그인 전용 `id` 속성 추가 - 테넌트 Config의 `loginIdField` 설정에 따라 User의 `login_id` 및 Kratos `traits.id` 동기화 로직 구현 - Admin UI 테넌트 스키마 설정 내 '로그인 ID로 사용' 체크박스 추가 - Admin UI 사용자 생성/수정/조회 화면에 로그인 ID 관리 필드 및 컬럼 반영 - Userfront 로그인 화면 접속 시 테넌트 설정에 따라 동적 로그인 ID 라벨 적용 - 관련 다국어(ko/en) 번역 추가 및 로그인 ID 설계 문서 업데이트
This commit is contained in:
@@ -321,11 +321,20 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
"grade": role,
|
||||
}
|
||||
|
||||
// [Resolve TenantID before Kratos creation]
|
||||
// [Resolve TenantID and LoginID before Kratos creation]
|
||||
var tenantID string
|
||||
if req.CompanyCode != "" && h.TenantService != nil {
|
||||
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), req.CompanyCode); err == nil && tenant != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
attributes["role"] = role
|
||||
@@ -333,6 +342,11 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
attributes["tenant_id"] = tenantID
|
||||
}
|
||||
|
||||
// [Override with explicit LoginID if provided]
|
||||
if req.LoginID != "" {
|
||||
attributes["id"] = req.LoginID
|
||||
}
|
||||
|
||||
// Merge custom metadata into attributes
|
||||
for k, v := range req.Metadata {
|
||||
// Don't overwrite core fields
|
||||
@@ -343,6 +357,7 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
|
||||
brokerUser := &domain.BrokerUser{
|
||||
Email: email,
|
||||
LoginID: extractString(attributes, "id"),
|
||||
Name: name,
|
||||
PhoneNumber: normalizePhoneNumber(req.Phone),
|
||||
Attributes: attributes,
|
||||
@@ -413,6 +428,7 @@ 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"`
|
||||
@@ -456,9 +472,10 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
|
||||
// Pre-fetch tenant data to avoid redundant DB calls
|
||||
type tenantCacheItem struct {
|
||||
ID string
|
||||
Schema []interface{}
|
||||
Groups []domain.UserGroup
|
||||
ID string
|
||||
Schema []interface{}
|
||||
Groups []domain.UserGroup
|
||||
LoginIDField string
|
||||
}
|
||||
tenantCache := make(map[string]tenantCacheItem)
|
||||
|
||||
@@ -500,6 +517,9 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||
tItem.Schema = s
|
||||
}
|
||||
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 {
|
||||
@@ -537,6 +557,20 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
"role": role,
|
||||
}
|
||||
|
||||
// Sync LoginID from configured custom field
|
||||
if tItem.LoginIDField != "" {
|
||||
if val, exists := item.Metadata[tItem.LoginIDField]; exists {
|
||||
if loginIDStr, ok := val.(string); ok && loginIDStr != "" {
|
||||
attributes["id"] = loginIDStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override with explicit LoginID if provided
|
||||
if item.LoginID != "" {
|
||||
attributes["id"] = item.LoginID
|
||||
}
|
||||
|
||||
// Merge metadata
|
||||
for k, v := range item.Metadata {
|
||||
if _, exists := attributes[k]; !exists {
|
||||
@@ -546,6 +580,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
|
||||
identityID, err := h.OryProvider.CreateUser(&domain.BrokerUser{
|
||||
Email: email,
|
||||
LoginID: extractTraitString(attributes, "id"),
|
||||
Name: item.Name,
|
||||
PhoneNumber: normalizePhoneNumber(item.Phone),
|
||||
Attributes: attributes,
|
||||
@@ -571,6 +606,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
localUser := &domain.User{
|
||||
ID: identityID,
|
||||
Email: email,
|
||||
LoginID: extractTraitString(attributes, "id"),
|
||||
Name: name,
|
||||
Phone: normalizePhoneNumber(item.Phone),
|
||||
Role: role,
|
||||
@@ -1014,6 +1050,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
LoginID *string `json:"loginId"`
|
||||
Password *string `json:"password"`
|
||||
Name *string `json:"name"`
|
||||
Phone *string `json:"phone"`
|
||||
@@ -1113,11 +1150,43 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
traits["role"] = role
|
||||
}
|
||||
|
||||
// [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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [Override with explicit LoginID if provided]
|
||||
if req.LoginID != nil && *req.LoginID != "" {
|
||||
traits["id"] = *req.LoginID
|
||||
}
|
||||
|
||||
// [Namespaced Metadata Sync]
|
||||
coreTraits := map[string]bool{
|
||||
"email": true, "name": true, "phone_number": true,
|
||||
"grade": true, "companyCode": true, "department": true,
|
||||
"affiliationType": true, "role": true, "tenant_id": true,
|
||||
"id": true,
|
||||
}
|
||||
|
||||
// For namespaced metadata, we don't delete everything, we merge.
|
||||
@@ -1284,6 +1353,7 @@ func (h *UserHandler) mapToLocalUser(identity service.KratosIdentity) *domain.Us
|
||||
user := &domain.User{
|
||||
ID: identity.ID,
|
||||
Email: extractTraitString(traits, "email"),
|
||||
LoginID: extractTraitString(traits, "id"),
|
||||
Name: extractTraitString(traits, "name"),
|
||||
Phone: extractTraitString(traits, "phone_number"),
|
||||
Role: role,
|
||||
|
||||
Reference in New Issue
Block a user