forked from baron/baron-sso
Implement tenant import and RP auto login policies
This commit is contained in:
@@ -425,6 +425,15 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
attributes["tenant_id"] = tenantID
|
||||
}
|
||||
|
||||
if h.UserRepo != nil {
|
||||
if err := h.ensureHanmacCreateEmailAllowed(c.Context(), email, req.CompanyCode, tenantID); err != nil {
|
||||
if strings.Contains(err.Error(), "한맥가족") {
|
||||
return errorJSON(c, fiber.StatusConflict, err.Error())
|
||||
}
|
||||
return errorJSON(c, fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Merge custom metadata into attributes
|
||||
for k, v := range req.Metadata {
|
||||
// Don't overwrite core fields
|
||||
@@ -534,10 +543,14 @@ type bulkUserItem struct {
|
||||
}
|
||||
|
||||
type bulkUserResult struct {
|
||||
Email string `json:"email"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
Email string `json:"email"`
|
||||
OriginalEmail string `json:"originalEmail,omitempty"`
|
||||
SuggestedEmail string `json:"suggestedEmail,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
@@ -565,6 +578,9 @@ 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
|
||||
hanmacScopeLoaded := false
|
||||
|
||||
// Pre-fetch tenant data to avoid redundant DB calls
|
||||
type tenantCacheItem struct {
|
||||
@@ -638,6 +654,53 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
if h.UserRepo != nil && !hanmacScopeLoaded {
|
||||
hanmacScopeLoaded = true
|
||||
var err error
|
||||
hanmacScope, err = h.resolveHanmacEmailScope(c.Context())
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "failed to resolve Hanmac family tenant scope"})
|
||||
continue
|
||||
}
|
||||
if hanmacScope != nil {
|
||||
hanmacLocalParts, err = h.loadHanmacLocalParts(c.Context(), hanmacScope)
|
||||
if err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "failed to validate Hanmac family email policy"})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userEmail := email
|
||||
var emailEvaluation hanmacEmailEvaluation
|
||||
if h.UserRepo != nil && hanmacScope != nil && hanmacScope.ContainsTenant(tItem.ID, tenantSlug) {
|
||||
emailEvaluation = h.evaluateHanmacImportEmail(c.Context(), item, hanmacScope, hanmacLocalParts)
|
||||
if emailEvaluation.Blocking {
|
||||
results = append(results, bulkUserResult{
|
||||
Email: emailEvaluation.Email,
|
||||
OriginalEmail: emailEvaluation.OriginalEmail,
|
||||
Status: emailEvaluation.Status,
|
||||
Warnings: emailEvaluation.Warnings,
|
||||
Success: false,
|
||||
Message: emailEvaluation.Message,
|
||||
})
|
||||
continue
|
||||
}
|
||||
userEmail = emailEvaluation.Email
|
||||
if emailEvaluation.LocalPart != "" {
|
||||
hanmacLocalParts[emailEvaluation.LocalPart] = true
|
||||
}
|
||||
} else {
|
||||
if _, _, err := domain.SplitEmailDomain(email); err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: "invalid email format"})
|
||||
continue
|
||||
}
|
||||
if localPart, err := domain.ExtractNormalizedEmailLocalPart(email); err != nil || localPart == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: "invalid email format"})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
password, _ := utils.GeneratePasswordWithPolicy(policy)
|
||||
role := item.Role
|
||||
if role == "" {
|
||||
@@ -665,7 +728,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
userEmail := email
|
||||
userPhone := normalizePhoneNumber(item.Phone)
|
||||
|
||||
// Validate all collected LoginIDs
|
||||
@@ -673,7 +735,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
valid := true
|
||||
for _, lid := range collectedIDs {
|
||||
if err := domain.ValidateLoginID(lid, userEmail, userPhone); err != nil {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "Invalid LoginID (" + lid + "): " + err.Error()})
|
||||
results = append(results, bulkUserResult{Email: userEmail, OriginalEmail: emailEvaluation.OriginalEmail, SuggestedEmail: emailEvaluation.SuggestedEmail, Status: emailEvaluation.Status, Warnings: emailEvaluation.Warnings, Success: false, Message: "Invalid LoginID (" + lid + "): " + err.Error()})
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
@@ -692,14 +754,14 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
// 만약 이미 존재하는 사용자라면 로컬 DB 및 Keto 관계만 업데이트(Sync)를 시도
|
||||
if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "exists already") {
|
||||
identityID, err = h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), email)
|
||||
identityID, err = h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), userEmail)
|
||||
if err != nil || identityID == "" {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: "이미 다른 사용자가 해당 식별자(이메일/사번 등)를 사용 중입니다."})
|
||||
results = append(results, bulkUserResult{Email: userEmail, OriginalEmail: emailEvaluation.OriginalEmail, SuggestedEmail: emailEvaluation.SuggestedEmail, Status: "blockingError", Warnings: emailEvaluation.Warnings, Success: false, Message: "이미 다른 사용자가 해당 식별자(이메일/사번 등)를 사용 중입니다."})
|
||||
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()})
|
||||
results = append(results, bulkUserResult{Email: userEmail, OriginalEmail: emailEvaluation.OriginalEmail, SuggestedEmail: emailEvaluation.SuggestedEmail, Status: emailEvaluation.Status, Warnings: emailEvaluation.Warnings, Success: false, Message: err.Error()})
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -709,7 +771,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
if h.UserRepo != nil {
|
||||
localUser := &domain.User{
|
||||
ID: identityID,
|
||||
Email: email,
|
||||
Email: userEmail,
|
||||
Name: name,
|
||||
Phone: normalizePhoneNumber(item.Phone),
|
||||
Role: role,
|
||||
@@ -776,7 +838,15 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, bulkUserResult{Email: email, Success: true, UserID: identityID})
|
||||
results = append(results, bulkUserResult{
|
||||
Email: userEmail,
|
||||
OriginalEmail: emailEvaluation.OriginalEmail,
|
||||
SuggestedEmail: emailEvaluation.SuggestedEmail,
|
||||
Status: emailEvaluation.Status,
|
||||
Warnings: emailEvaluation.Warnings,
|
||||
Success: true,
|
||||
UserID: identityID,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
@@ -870,12 +940,19 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
defer writer.Flush()
|
||||
|
||||
// Header row
|
||||
header := []string{"ID", "Email", "Name", "Phone", "Status", "Tenant", "Position", "JobTitle", "CreatedAt"}
|
||||
includeIDs := includeCSVIds(c)
|
||||
header := []string{"Email", "Name", "Phone", "Status", "tenant_slug", "Position", "JobTitle", "CreatedAt"}
|
||||
if includeIDs {
|
||||
header = []string{"user_id", "Email", "Name", "Phone", "Status", "tenant_id", "tenant_slug", "Position", "JobTitle", "CreatedAt"}
|
||||
}
|
||||
|
||||
// Collect all possible metadata keys for dynamic columns
|
||||
metaKeysMap := make(map[string]bool)
|
||||
for _, u := range filtered {
|
||||
for k := range u.Metadata {
|
||||
if !includeIDs && csvMetadataKeyIsID(k) {
|
||||
continue
|
||||
}
|
||||
metaKeysMap[k] = true
|
||||
}
|
||||
}
|
||||
@@ -891,8 +968,11 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
|
||||
// Data rows
|
||||
for _, u := range filtered {
|
||||
tenantID := ""
|
||||
if u.TenantID != nil {
|
||||
tenantID = *u.TenantID
|
||||
}
|
||||
row := []string{
|
||||
u.ID,
|
||||
u.Email,
|
||||
u.Name,
|
||||
u.Phone,
|
||||
@@ -902,6 +982,20 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
u.JobTitle,
|
||||
u.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
if includeIDs {
|
||||
row = []string{
|
||||
u.ID,
|
||||
u.Email,
|
||||
u.Name,
|
||||
u.Phone,
|
||||
u.Status,
|
||||
tenantID,
|
||||
u.CompanyCode,
|
||||
u.Position,
|
||||
u.JobTitle,
|
||||
u.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
// Append metadata values in order
|
||||
for _, k := range metaKeys {
|
||||
val := ""
|
||||
@@ -918,6 +1012,11 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func csvMetadataKeyIsID(key string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(key))
|
||||
return normalized == "id" || normalized == "user_id" || normalized == "tenant_id" || normalized == "tenantid"
|
||||
}
|
||||
|
||||
func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
UserIDs []string `json:"userIds"`
|
||||
|
||||
Reference in New Issue
Block a user