package domain import ( "fmt" "slices" "strings" "time" "github.com/google/uuid" "github.com/lib/pq" "gorm.io/gorm" ) // User roles const ( RoleSuperAdmin = "super_admin" // 시스템 전역 관리자 RoleUser = "user" // 일반 사용자 ) // User statuses const ( UserStatusActive = "active" UserStatusSuspended = "suspended" UserStatusTemporaryLeave = "temporary_leave" UserStatusPreboarding = "preboarding" UserStatusBaronGuest = "baron_guest" UserStatusExtendedLeave = "extended_leave" UserStatusArchived = "archived" ) func NormalizeUserStatus(status string) string { switch strings.ToLower(strings.TrimSpace(status)) { case "", UserStatusActive: return UserStatusActive case "blocked", UserStatusSuspended: return UserStatusSuspended case "inactive", UserStatusPreboarding: return UserStatusPreboarding case "leave_of_absence", UserStatusTemporaryLeave: return UserStatusTemporaryLeave case "baron_only", UserStatusBaronGuest: return UserStatusBaronGuest case UserStatusExtendedLeave: return UserStatusExtendedLeave case UserStatusArchived: return UserStatusArchived default: return strings.ToLower(strings.TrimSpace(status)) } } func IsBaronActivityAllowedStatus(status string) bool { switch NormalizeUserStatus(status) { case UserStatusActive, UserStatusTemporaryLeave, UserStatusBaronGuest: return true default: return false } } func IsOrgVisibleUserStatus(status string) bool { switch NormalizeUserStatus(status) { case UserStatusActive, UserStatusTemporaryLeave, UserStatusSuspended: return true default: return false } } func IsWorksProvisionedUserStatus(status string) bool { switch NormalizeUserStatus(status) { case UserStatusActive, UserStatusTemporaryLeave, UserStatusSuspended: return true default: return false } } func IsWorksDeprovisionUserStatus(status string) bool { switch NormalizeUserStatus(status) { case UserStatusBaronGuest, UserStatusExtendedLeave, UserStatusArchived: return true default: return false } } // NormalizeRole maps legacy/synonym role values to canonical role keys. func NormalizeRole(role string) string { if normalized, ok := NormalizeRoleAlias(role); ok { return normalized } return RoleUser } func NormalizeRoleAlias(role string) (string, bool) { normalized := strings.ToLower(strings.TrimSpace(role)) switch normalized { case RoleSuperAdmin, RoleUser: return normalized, true case "tenant_admin", "rp_admin", "tenant_member", "member", "admin", "tenantadmin", "tenant-admin": return RoleUser, true case "superadmin", "super-admin": return RoleSuperAdmin, true default: return "", false } } // User represents the user model stored in PostgreSQL type User struct { ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"` Email string `gorm:"uniqueIndex;not null" json:"email"` PasswordHash *string `gorm:"column:password_hash" json:"-"` Name string `gorm:"column:name;not null" json:"name"` Phone string `gorm:"column:phone" json:"phone"` Role string `gorm:"column:role;default:'user';not null" json:"role"` // super_admin, user AffiliationType string `gorm:"column:affiliation_type" json:"affiliationType"` CompanyCode string `gorm:"-" json:"companyCode,omitempty"` CompanyCodes pq.StringArray `gorm:"-" json:"companyCodes,omitempty"` TenantID *string `gorm:"column:tenant_id;type:uuid;index" json:"tenantId,omitempty"` Tenant *Tenant `gorm:"foreignKey:TenantID" json:"tenant,omitempty"` RelyingPartyID *string `gorm:"column:relying_party_id;type:uuid;index" json:"relyingPartyId,omitempty"` // RP Admin용 Department string `gorm:"column:department" json:"department"` Grade string `gorm:"column:grade" json:"grade"` // 직급 (예: 수석, 책임, 선임) Position string `gorm:"column:position" json:"position"` // 직책 (예: 팀장, 센터장) JobTitle string `gorm:"column:job_title" json:"jobTitle"` // 직무 (예: 프론트엔드 개발, 기획) Metadata JSONMap `gorm:"column:metadata;type:jsonb" json:"metadata,omitempty"` Status string `gorm:"column:status;default:'active'" json:"status"` CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"` // Multiple identifiers support UserLoginIDs []UserLoginID `gorm:"foreignKey:UserID" json:"userLoginIds,omitempty"` } // UserLoginID represents multiple custom identifiers for a user type UserLoginID struct { ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"` UserID string `gorm:"type:uuid;not null;index" json:"userId"` TenantID string `gorm:"type:uuid;not null;index" json:"tenantId"` // 발급 테넌트 FieldKey string `gorm:"not null" json:"fieldKey"` // 스키마 필드 키 (예: emp_id) LoginID string `gorm:"uniqueIndex;not null" json:"loginId"` // 실제 값 (예: EMP001) } // BeforeCreate hook to generate UUID if not present func (u *User) BeforeCreate(tx *gorm.DB) (err error) { if u.ID == "" { u.ID = uuid.New().String() } return } // ValidateLoginID checks if the loginID violates any collision, length, or security rules. func ValidateLoginID(loginID string, emails []string, phone string) error { loginID = strings.TrimSpace(loginID) if loginID == "" { return nil } if len(loginID) < 4 || len(loginID) > 30 { return fmt.Errorf("ID must be between 4 and 30 characters") } if strings.Contains(loginID, "@") { return fmt.Errorf("ID cannot be an email format") } for _, email := range emails { if email != "" && strings.EqualFold(loginID, email) { return fmt.Errorf("ID cannot be the same as the email address") } } if phone != "" { normalizedPhone := strings.ReplaceAll(phone, "-", "") normalizedPhone = strings.ReplaceAll(normalizedPhone, " ", "") if strings.HasPrefix(normalizedPhone, "010") { normalizedPhone = "+82" + normalizedPhone[1:] } else if strings.HasPrefix(normalizedPhone, "82") { normalizedPhone = "+" + normalizedPhone } if loginID == phone || loginID == normalizedPhone { return fmt.Errorf("ID cannot be the same as the phone number") } } isPureNumber := true loginIDDigits := strings.ReplaceAll(loginID, "-", "") loginIDDigits = strings.ReplaceAll(loginIDDigits, " ", "") for _, c := range loginIDDigits { if (c < '0' || c > '9') && c != '+' { isPureNumber = false break } } if isPureNumber && len(loginIDDigits) >= 10 && len(loginIDDigits) <= 12 { if strings.HasPrefix(loginIDDigits, "010") || strings.HasPrefix(loginIDDigits, "82") || strings.HasPrefix(loginIDDigits, "+82") { return fmt.Errorf("ID cannot be a phone number format") } } reserved := []string{"admin", "system", "root", "master", "superuser", "guest", "operator"} lowerID := strings.ToLower(loginID) if slices.Contains(reserved, lowerID) { return fmt.Errorf("reserved ID cannot be used") } return nil }