forked from baron/baron-sso
322 lines
8.5 KiB
Go
322 lines
8.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const hanmacFamilyTenantSlug = "hanmac-family"
|
|
|
|
type hanmacEmailScope struct {
|
|
TenantIDs map[string]bool
|
|
Slugs map[string]bool
|
|
IDList []string
|
|
SlugList []string
|
|
}
|
|
|
|
type hanmacEmailEvaluation struct {
|
|
Email string
|
|
OriginalEmail string
|
|
SuggestedEmail string
|
|
Status string
|
|
Warnings []string
|
|
Message string
|
|
Blocking bool
|
|
LocalPart string
|
|
}
|
|
|
|
type hanmacLocalPartOwner struct {
|
|
UserID string
|
|
Email string
|
|
Name string
|
|
}
|
|
|
|
func (h *UserHandler) evaluateHanmacImportEmail(ctx context.Context, item bulkUserItem, scope *hanmacEmailScope, usedLocalParts map[string]hanmacLocalPartOwner) hanmacEmailEvaluation {
|
|
originalEmail := strings.TrimSpace(item.Email)
|
|
name := strings.TrimSpace(item.Name)
|
|
evaluation := hanmacEmailEvaluation{
|
|
Email: originalEmail,
|
|
OriginalEmail: originalEmail,
|
|
Status: "valid",
|
|
}
|
|
|
|
localPart, domainPart, err := domain.SplitEmailDomain(originalEmail)
|
|
if err != nil {
|
|
evaluation.Status = "blockingError"
|
|
evaluation.Message = "invalid email format"
|
|
evaluation.Blocking = true
|
|
return evaluation
|
|
}
|
|
|
|
base, needsReview, _ := domain.BuildKoreanNameEmailBase(name)
|
|
if needsReview {
|
|
evaluation.Warnings = append(evaluation.Warnings, "needsReview")
|
|
evaluation.Status = "needsReview"
|
|
}
|
|
|
|
if localPart == "" {
|
|
if base == "" {
|
|
evaluation.Status = "blockingError"
|
|
evaluation.Message = "이름으로 이메일 ID를 제안할 수 없습니다."
|
|
evaluation.Blocking = true
|
|
return evaluation
|
|
}
|
|
nextLocalPart := nextAvailableHanmacLocalPart(base, usedLocalParts)
|
|
evaluation.Email = nextLocalPart + "@" + domainPart
|
|
evaluation.SuggestedEmail = evaluation.Email
|
|
evaluation.LocalPart = nextLocalPart
|
|
evaluation.Status = "suggested"
|
|
evaluation.Warnings = appendUniqueString(evaluation.Warnings, "suggested")
|
|
return evaluation
|
|
}
|
|
|
|
evaluation.LocalPart = localPart
|
|
if owner, exists := usedLocalParts[localPart]; exists {
|
|
evaluation.Status = "blockingError"
|
|
evaluation.Message = formatHanmacLocalPartConflictMessage(localPart, owner)
|
|
evaluation.Blocking = true
|
|
return evaluation
|
|
}
|
|
|
|
if base != "" && !domain.MatchesSuggestedNameRule(localPart, base) {
|
|
evaluation.Status = "ruleMismatch"
|
|
evaluation.Warnings = appendUniqueString(evaluation.Warnings, "ruleMismatch")
|
|
}
|
|
|
|
if evaluation.Status == "needsReview" && len(evaluation.Warnings) == 0 {
|
|
evaluation.Warnings = append(evaluation.Warnings, "needsReview")
|
|
}
|
|
_ = scope
|
|
return evaluation
|
|
}
|
|
|
|
func (h *UserHandler) ensureHanmacCreateEmailAllowed(ctx context.Context, email string, tenantSlug string, tenantID string) error {
|
|
return h.ensureHanmacEmailAllowedWithLog(ctx, email, tenantSlug, tenantID, "", "hanmac create email local-part conflict")
|
|
}
|
|
|
|
func (h *UserHandler) ensureHanmacEmailAllowed(ctx context.Context, email string, tenantSlug string, tenantID string, currentUserID string) error {
|
|
return h.ensureHanmacEmailAllowedWithLog(ctx, email, tenantSlug, tenantID, currentUserID, "hanmac email local-part conflict")
|
|
}
|
|
|
|
func (h *UserHandler) ensureHanmacEmailAllowedWithLog(ctx context.Context, email string, tenantSlug string, tenantID string, currentUserID string, logMessage string) error {
|
|
scope, err := h.resolveHanmacEmailScope(ctx)
|
|
if err != nil || scope == nil || !scope.ContainsTenant(tenantID, tenantSlug) {
|
|
return nil
|
|
}
|
|
|
|
localPart, err := domain.ExtractNormalizedEmailLocalPart(email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
usedLocalParts, err := h.loadHanmacLocalParts(ctx, scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if owner, exists := usedLocalParts[localPart]; exists {
|
|
ownerUserID := strings.TrimSpace(owner.UserID)
|
|
if currentUserID != "" && ownerUserID != "" && ownerUserID == strings.TrimSpace(currentUserID) {
|
|
return nil
|
|
}
|
|
slog.Warn(
|
|
logMessage,
|
|
"requestedEmail", email,
|
|
"localPart", localPart,
|
|
"ownerUserID", owner.UserID,
|
|
"ownerEmail", owner.Email,
|
|
"ownerName", owner.Name,
|
|
"tenantID", tenantID,
|
|
"tenantSlug", tenantSlug,
|
|
)
|
|
return fmt.Errorf("%s", formatHanmacLocalPartConflictMessage(localPart, owner))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *UserHandler) resolveHanmacEmailScope(ctx context.Context) (*hanmacEmailScope, error) {
|
|
if h.TenantService == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
tenants, _, err := h.TenantService.ListTenants(ctx, 10000, 0, "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rootID string
|
|
for _, tenant := range tenants {
|
|
if strings.EqualFold(strings.TrimSpace(tenant.Slug), hanmacFamilyTenantSlug) {
|
|
rootID = tenant.ID
|
|
break
|
|
}
|
|
}
|
|
if rootID == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
tenantByID := make(map[string]domain.Tenant, len(tenants))
|
|
for _, tenant := range tenants {
|
|
tenantByID[tenant.ID] = tenant
|
|
}
|
|
|
|
scope := &hanmacEmailScope{
|
|
TenantIDs: make(map[string]bool),
|
|
Slugs: make(map[string]bool),
|
|
}
|
|
for _, tenant := range tenants {
|
|
if isTenantDescendantOf(tenant, rootID, tenantByID) {
|
|
scope.TenantIDs[tenant.ID] = true
|
|
scope.Slugs[strings.ToLower(strings.TrimSpace(tenant.Slug))] = true
|
|
scope.IDList = append(scope.IDList, tenant.ID)
|
|
scope.SlugList = append(scope.SlugList, tenant.Slug)
|
|
}
|
|
}
|
|
return scope, nil
|
|
}
|
|
|
|
func (h *UserHandler) loadHanmacLocalParts(ctx context.Context, scope *hanmacEmailScope) (map[string]hanmacLocalPartOwner, error) {
|
|
used := make(map[string]hanmacLocalPartOwner)
|
|
if h.UserRepo == nil || scope == nil {
|
|
return used, nil
|
|
}
|
|
|
|
if len(scope.IDList) > 0 {
|
|
users, err := h.UserRepo.FindByTenantIDs(ctx, scope.IDList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addUserEmailLocalPartOwners(used, users)
|
|
}
|
|
|
|
if len(scope.SlugList) > 0 {
|
|
users, err := h.UserRepo.FindByCompanyCodes(ctx, scope.SlugList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addUserEmailLocalPartOwners(used, users)
|
|
}
|
|
|
|
return used, nil
|
|
}
|
|
|
|
func (s *hanmacEmailScope) ContainsTenant(tenantID string, slug string) bool {
|
|
if s == nil {
|
|
return false
|
|
}
|
|
if tenantID != "" && s.TenantIDs[tenantID] {
|
|
return true
|
|
}
|
|
return s.Slugs[strings.ToLower(strings.TrimSpace(slug))]
|
|
}
|
|
|
|
func isTenantDescendantOf(tenant domain.Tenant, rootID string, tenantByID map[string]domain.Tenant) bool {
|
|
if tenant.ID == rootID {
|
|
return true
|
|
}
|
|
visited := make(map[string]bool)
|
|
parentID := ""
|
|
if tenant.ParentID != nil {
|
|
parentID = *tenant.ParentID
|
|
}
|
|
for parentID != "" {
|
|
if parentID == rootID {
|
|
return true
|
|
}
|
|
if visited[parentID] {
|
|
return false
|
|
}
|
|
visited[parentID] = true
|
|
parent, ok := tenantByID[parentID]
|
|
if !ok || parent.ParentID == nil {
|
|
return false
|
|
}
|
|
parentID = *parent.ParentID
|
|
}
|
|
return false
|
|
}
|
|
|
|
func addUserEmailLocalPartOwners(target map[string]hanmacLocalPartOwner, users []domain.User) {
|
|
for _, user := range users {
|
|
localPart, err := domain.ExtractNormalizedEmailLocalPart(user.Email)
|
|
if err != nil || localPart == "" {
|
|
continue
|
|
}
|
|
if _, exists := target[localPart]; exists {
|
|
continue
|
|
}
|
|
target[localPart] = hanmacLocalPartOwner{
|
|
UserID: strings.TrimSpace(user.ID),
|
|
Email: strings.TrimSpace(user.Email),
|
|
Name: strings.TrimSpace(user.Name),
|
|
}
|
|
}
|
|
}
|
|
|
|
func formatHanmacLocalPartConflictMessage(localPart string, owner hanmacLocalPartOwner) string {
|
|
message := fmt.Sprintf("한맥가족 내에서 이미 사용 중인 이메일 ID입니다. local-part=%s", strings.TrimSpace(localPart))
|
|
if owner.Email != "" {
|
|
message += ", 사용 계정=" + owner.Email
|
|
}
|
|
if owner.Name != "" {
|
|
message += ", 사용자=" + owner.Name
|
|
}
|
|
if owner.UserID != "" {
|
|
message += ", 사용자 ID=" + owner.UserID
|
|
}
|
|
return message
|
|
}
|
|
|
|
func nextAvailableHanmacLocalPart(base string, usedLocalParts map[string]hanmacLocalPartOwner) string {
|
|
base = strings.ToLower(strings.TrimSpace(base))
|
|
if base == "" {
|
|
return ""
|
|
}
|
|
if _, exists := usedLocalParts[base]; !exists {
|
|
return base
|
|
}
|
|
|
|
stem, nextIndex := splitTrailingNumericSuffix(base)
|
|
if stem == "" {
|
|
stem = base
|
|
}
|
|
for index := nextIndex; ; index++ {
|
|
candidate := fmt.Sprintf("%s%d", stem, index)
|
|
if _, exists := usedLocalParts[candidate]; !exists {
|
|
return candidate
|
|
}
|
|
}
|
|
}
|
|
|
|
func splitTrailingNumericSuffix(value string) (string, int) {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value == "" {
|
|
return "", 1
|
|
}
|
|
index := len(value)
|
|
for index > 0 && value[index-1] >= '0' && value[index-1] <= '9' {
|
|
index--
|
|
}
|
|
if index == len(value) {
|
|
return value, 1
|
|
}
|
|
stem := value[:index]
|
|
suffix := value[index:]
|
|
number, err := strconv.Atoi(suffix)
|
|
if err != nil {
|
|
return value, 1
|
|
}
|
|
return stem, number + 1
|
|
}
|
|
|
|
func appendUniqueString(values []string, value string) []string {
|
|
if slices.Contains(values, value) {
|
|
return values
|
|
}
|
|
return append(values, value)
|
|
}
|