forked from baron/baron-sso
@@ -34,9 +34,10 @@ type UserHandler struct {
|
||||
KetoService service.KetoService
|
||||
KetoOutboxRepo repository.KetoOutboxRepository
|
||||
UserRepo repository.UserRepository
|
||||
UserGroupRepo repository.UserGroupRepository
|
||||
}
|
||||
|
||||
func NewUserHandler(kratosAdmin service.KratosAdminService, oryProvider OryProviderAPI, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository) *UserHandler {
|
||||
func NewUserHandler(kratosAdmin service.KratosAdminService, oryProvider OryProviderAPI, tenantService service.TenantService, ketoService service.KetoService, ketoOutboxRepo repository.KetoOutboxRepository, userRepo repository.UserRepository, userGroupRepo repository.UserGroupRepository) *UserHandler {
|
||||
return &UserHandler{
|
||||
KratosAdmin: kratosAdmin,
|
||||
OryProvider: oryProvider,
|
||||
@@ -44,6 +45,7 @@ func NewUserHandler(kratosAdmin service.KratosAdminService, oryProvider OryProvi
|
||||
KetoService: ketoService,
|
||||
KetoOutboxRepo: ketoOutboxRepo,
|
||||
UserRepo: userRepo,
|
||||
UserGroupRepo: userGroupRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
limit := c.QueryInt("limit", 50)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
companyCode := strings.TrimSpace(c.Query("companyCode"))
|
||||
tenantSlug := strings.TrimSpace(c.Query("tenantSlug"))
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
@@ -125,8 +127,8 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated companyCode filter
|
||||
if companyCode != "" && !strings.EqualFold(compCode, companyCode) {
|
||||
// Dedicated tenantSlug filter
|
||||
if tenantSlug != "" && !strings.EqualFold(compCode, tenantSlug) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -176,7 +178,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
slog.Warn("Kratos unavailable, falling back to local DB for user list", "error", err)
|
||||
|
||||
// Fetch from UserRepo
|
||||
users, total, err := h.UserRepo.List(c.Context(), offset, limit, search, companyCode)
|
||||
users, total, err := h.UserRepo.List(c.Context(), offset, limit, search, tenantSlug)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch users from both kratos and local db")
|
||||
}
|
||||
@@ -414,7 +416,7 @@ type bulkUserItem struct {
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
CompanyCode string `json:"companyCode"`
|
||||
TenantSlug string `json:"tenantSlug"`
|
||||
Department string `json:"department"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
@@ -456,13 +458,14 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
type tenantCacheItem struct {
|
||||
ID string
|
||||
Schema []interface{}
|
||||
Groups []domain.UserGroup
|
||||
}
|
||||
tenantCache := make(map[string]tenantCacheItem)
|
||||
|
||||
for _, item := range req.Users {
|
||||
email := strings.TrimSpace(item.Email)
|
||||
name := strings.TrimSpace(item.Name)
|
||||
compCode := strings.TrimSpace(item.CompanyCode)
|
||||
tenantSlug := strings.TrimSpace(item.TenantSlug)
|
||||
dept := strings.TrimSpace(item.Department)
|
||||
|
||||
if email == "" || name == "" {
|
||||
@@ -470,14 +473,14 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if compCode == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "companyCode (tenant) is required"})
|
||||
if tenantSlug == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenantSlug is required"})
|
||||
continue
|
||||
}
|
||||
|
||||
// Role-based access check
|
||||
if requester != nil && requester.Role == domain.RoleTenantAdmin {
|
||||
if compCode != requester.CompanyCode {
|
||||
if tenantSlug != requester.CompanyCode {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "forbidden: cannot add users to another tenant"})
|
||||
continue
|
||||
}
|
||||
@@ -486,18 +489,25 @@ 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[compCode]; !exists {
|
||||
if tItem, exists = tenantCache[tenantSlug]; !exists {
|
||||
if h.TenantService != nil {
|
||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), compCode)
|
||||
tenant, err := h.TenantService.GetTenantBySlug(c.Context(), tenantSlug)
|
||||
if err != nil || tenant == nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "invalid companyCode: tenant not found"})
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "invalid tenantSlug: tenant not found"})
|
||||
continue
|
||||
}
|
||||
tItem.ID = tenant.ID
|
||||
if s, ok := tenant.Config["userSchema"].([]interface{}); ok {
|
||||
tItem.Schema = s
|
||||
}
|
||||
tenantCache[compCode] = tItem
|
||||
// [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
|
||||
}
|
||||
}
|
||||
|
||||
tenantCache[tenantSlug] = tItem
|
||||
} else {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "tenant service unavailable"})
|
||||
continue
|
||||
@@ -521,7 +531,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
attributes := map[string]interface{}{
|
||||
"department": dept,
|
||||
"affiliationType": "internal",
|
||||
"companyCode": compCode,
|
||||
"companyCode": tenantSlug,
|
||||
"tenant_id": tItem.ID,
|
||||
"grade": role,
|
||||
"role": role,
|
||||
@@ -541,8 +551,18 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
Attributes: attributes,
|
||||
}, password)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
|
||||
continue
|
||||
// 만약 이미 존재하는 사용자라면 로컬 DB 및 Keto 관계만 업데이트(Sync)를 시도
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
identityID, err = h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), email)
|
||||
if err != nil || identityID == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "이미 존재하는 사용자지만 ID를 찾을 수 없습니다."})
|
||||
continue
|
||||
}
|
||||
slog.Info("BulkCreate: User already exists, syncing local DB and Keto", "email", email, "identityID", identityID)
|
||||
} else {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: err.Error()})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// [CRITICAL FIX] Sync to local DB directly using current data
|
||||
@@ -555,7 +575,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
Phone: normalizePhoneNumber(item.Phone),
|
||||
Role: role,
|
||||
Status: "active",
|
||||
CompanyCode: compCode,
|
||||
CompanyCode: tenantSlug,
|
||||
Department: dept,
|
||||
AffiliationType: "internal",
|
||||
CreatedAt: time.Now(),
|
||||
@@ -590,6 +610,22 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
}
|
||||
|
||||
// 3. Sync membership to UserGroup if department matches
|
||||
if dept != "" {
|
||||
for _, g := range tItem.Groups {
|
||||
if strings.EqualFold(strings.TrimSpace(g.Name), dept) {
|
||||
_ = h.KetoOutboxRepo.Create(c.Context(), &domain.KetoOutbox{
|
||||
Namespace: "Tenant",
|
||||
Object: g.ID,
|
||||
Relation: "members",
|
||||
Subject: "User:" + localUser.ID,
|
||||
Action: domain.KetoOutboxActionCreate,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user