1
0
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:
2026-03-25 15:27:44 +09:00
parent 8cadd82a2b
commit d10f80d41d
18 changed files with 799 additions and 420 deletions

View File

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