forked from baron/baron-sso
1347 lines
44 KiB
Go
1347 lines
44 KiB
Go
package service
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net/mail"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
WorksmobileUserActionUpsert = "UPSERT"
|
|
WorksmobileUserActionSuspend = "SUSPEND"
|
|
)
|
|
|
|
type WorksmobileOrgUnitPayload struct {
|
|
DomainID int64 `json:"domainId"`
|
|
OrgUnitName string `json:"orgUnitName"`
|
|
Email string `json:"email,omitempty"`
|
|
OrgUnitExternalKey string `json:"orgUnitExternalKey"`
|
|
ParentOrgUnitID string `json:"parentOrgUnitId,omitempty"`
|
|
DisplayOrder int `json:"displayOrder"`
|
|
}
|
|
|
|
type WorksmobileUserPayload struct {
|
|
DomainID int64 `json:"domainId"`
|
|
Email string `json:"email"`
|
|
UserExternalKey string `json:"userExternalKey,omitempty"`
|
|
UserName WorksmobileUserName `json:"userName"`
|
|
CellPhone string `json:"cellPhone,omitempty"`
|
|
EmployeeNumber string `json:"employeeNumber,omitempty"`
|
|
PrivateEmail string `json:"privateEmail,omitempty"`
|
|
AliasEmails []string `json:"aliasEmails,omitempty"`
|
|
Locale string `json:"locale,omitempty"`
|
|
LevelID string `json:"-"`
|
|
LevelDomainID int64 `json:"levelDomainId,omitempty"`
|
|
PasswordConfig WorksmobilePasswordConfig `json:"passwordConfig,omitempty"`
|
|
Task string `json:"task,omitempty"`
|
|
Organizations []WorksmobileUserOrganization `json:"organizations,omitempty"`
|
|
}
|
|
|
|
type WorksmobileUserName struct {
|
|
LastName string `json:"lastName,omitempty"`
|
|
}
|
|
|
|
type WorksmobilePasswordConfig struct {
|
|
PasswordCreationType string `json:"passwordCreationType"`
|
|
Password string `json:"password"`
|
|
ChangePasswordAtNextLogin *bool `json:"changePasswordAtNextLogin,omitempty"`
|
|
}
|
|
|
|
func (c WorksmobilePasswordConfig) IsZero() bool {
|
|
return strings.TrimSpace(c.PasswordCreationType) == "" &&
|
|
strings.TrimSpace(c.Password) == "" &&
|
|
c.ChangePasswordAtNextLogin == nil
|
|
}
|
|
|
|
func (p WorksmobileUserPayload) MarshalJSON() ([]byte, error) {
|
|
type payloadJSON struct {
|
|
DomainID int64 `json:"domainId"`
|
|
Email string `json:"email"`
|
|
UserExternalKey string `json:"userExternalKey,omitempty"`
|
|
UserName WorksmobileUserName `json:"userName"`
|
|
CellPhone string `json:"cellPhone,omitempty"`
|
|
EmployeeNumber string `json:"employeeNumber,omitempty"`
|
|
PrivateEmail string `json:"privateEmail,omitempty"`
|
|
AliasEmails []string `json:"aliasEmails,omitempty"`
|
|
Locale string `json:"locale,omitempty"`
|
|
LevelName string `json:"levelName,omitempty"`
|
|
LevelDomainID int64 `json:"levelDomainId,omitempty"`
|
|
PasswordConfig *WorksmobilePasswordConfig `json:"passwordConfig,omitempty"`
|
|
Task string `json:"task,omitempty"`
|
|
Organizations []WorksmobileUserOrganization `json:"organizations,omitempty"`
|
|
}
|
|
|
|
var passwordConfig *WorksmobilePasswordConfig
|
|
if !p.PasswordConfig.IsZero() {
|
|
passwordConfig = &p.PasswordConfig
|
|
}
|
|
|
|
return json.Marshal(payloadJSON{
|
|
DomainID: p.DomainID,
|
|
Email: p.Email,
|
|
UserExternalKey: p.UserExternalKey,
|
|
UserName: p.UserName,
|
|
CellPhone: p.CellPhone,
|
|
EmployeeNumber: p.EmployeeNumber,
|
|
PrivateEmail: p.PrivateEmail,
|
|
AliasEmails: p.AliasEmails,
|
|
Locale: p.Locale,
|
|
LevelName: strings.TrimSpace(p.LevelID),
|
|
LevelDomainID: p.LevelDomainID,
|
|
PasswordConfig: passwordConfig,
|
|
Task: p.Task,
|
|
Organizations: p.Organizations,
|
|
})
|
|
}
|
|
|
|
func (p *WorksmobileUserPayload) UnmarshalJSON(data []byte) error {
|
|
type payloadJSON struct {
|
|
DomainID int64 `json:"domainId"`
|
|
Email string `json:"email"`
|
|
UserExternalKey string `json:"userExternalKey,omitempty"`
|
|
UserName WorksmobileUserName `json:"userName"`
|
|
CellPhone string `json:"cellPhone,omitempty"`
|
|
EmployeeNumber string `json:"employeeNumber,omitempty"`
|
|
PrivateEmail string `json:"privateEmail,omitempty"`
|
|
AliasEmails []string `json:"aliasEmails,omitempty"`
|
|
Locale string `json:"locale,omitempty"`
|
|
LevelID string `json:"levelId,omitempty"`
|
|
LevelName string `json:"levelName,omitempty"`
|
|
LevelDomainID int64 `json:"levelDomainId,omitempty"`
|
|
Level *WorksmobileUserLevelRef `json:"level,omitempty"`
|
|
PasswordConfig WorksmobilePasswordConfig `json:"passwordConfig,omitempty"`
|
|
Task string `json:"task,omitempty"`
|
|
Organizations []WorksmobileUserOrganization `json:"organizations,omitempty"`
|
|
}
|
|
var raw payloadJSON
|
|
if err := json.Unmarshal(data, &raw); err != nil {
|
|
return err
|
|
}
|
|
levelID := strings.TrimSpace(raw.LevelName)
|
|
if levelID == "" {
|
|
levelID = strings.TrimSpace(raw.LevelID)
|
|
}
|
|
if levelID == "" && raw.Level != nil {
|
|
levelID = strings.TrimSpace(raw.Level.LevelID)
|
|
}
|
|
*p = WorksmobileUserPayload{
|
|
DomainID: raw.DomainID,
|
|
Email: raw.Email,
|
|
UserExternalKey: raw.UserExternalKey,
|
|
UserName: raw.UserName,
|
|
CellPhone: raw.CellPhone,
|
|
EmployeeNumber: raw.EmployeeNumber,
|
|
PrivateEmail: raw.PrivateEmail,
|
|
AliasEmails: raw.AliasEmails,
|
|
Locale: raw.Locale,
|
|
LevelID: levelID,
|
|
LevelDomainID: raw.LevelDomainID,
|
|
PasswordConfig: raw.PasswordConfig,
|
|
Task: raw.Task,
|
|
Organizations: raw.Organizations,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type WorksmobilePasswordResetPayload struct {
|
|
Email string `json:"email"`
|
|
PasswordConfig WorksmobilePasswordConfig `json:"passwordConfig"`
|
|
}
|
|
|
|
type WorksmobileUserOrganization struct {
|
|
DomainID int64 `json:"domainId,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
Primary bool `json:"primary"`
|
|
LevelID string `json:"levelId,omitempty"`
|
|
LevelName string `json:"levelName,omitempty"`
|
|
OrgUnits []WorksmobileUserOrgUnit `json:"orgUnits"`
|
|
}
|
|
|
|
type WorksmobileUserOrgUnit struct {
|
|
OrgUnitID string `json:"orgUnitId"`
|
|
Primary bool `json:"primary"`
|
|
PositionID string `json:"positionId,omitempty"`
|
|
IsManager *bool `json:"isManager,omitempty"`
|
|
}
|
|
|
|
func BuildWorksmobileOrgUnitPayload(tenant domain.Tenant, rootConfig domain.JSONMap, displayOrder int) (WorksmobileOrgUnitPayload, error) {
|
|
return BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant, tenant, rootConfig, displayOrder)
|
|
}
|
|
|
|
func BuildWorksmobileOrgUnitPayloadForDomainTenant(tenant domain.Tenant, domainTenant domain.Tenant, rootConfig domain.JSONMap, displayOrder int) (WorksmobileOrgUnitPayload, error) {
|
|
if err := ValidateWorksmobileExternalKey(tenant.ID); err != nil {
|
|
return WorksmobileOrgUnitPayload{}, err
|
|
}
|
|
if displayOrder < 1 {
|
|
displayOrder = 1
|
|
}
|
|
domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig)
|
|
if err != nil {
|
|
return WorksmobileOrgUnitPayload{}, err
|
|
}
|
|
payload := WorksmobileOrgUnitPayload{
|
|
DomainID: domainID,
|
|
OrgUnitName: strings.TrimSpace(tenant.Name),
|
|
Email: buildWorksmobileOrgUnitEmail(tenant, domainTenant),
|
|
OrgUnitExternalKey: tenant.ID,
|
|
DisplayOrder: displayOrder,
|
|
}
|
|
if tenant.ParentID != nil && *tenant.ParentID != "" {
|
|
if err := ValidateWorksmobileExternalKey(*tenant.ParentID); err != nil {
|
|
return WorksmobileOrgUnitPayload{}, err
|
|
}
|
|
payload.ParentOrgUnitID = "externalKey:" + *tenant.ParentID
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func buildWorksmobileOrgUnitEmail(tenant domain.Tenant, domainTenant domain.Tenant) string {
|
|
slug := strings.ToLower(strings.TrimSpace(tenant.Slug))
|
|
if slug == "" {
|
|
return ""
|
|
}
|
|
if domainName := worksmobileTenantMailDomain(domainTenant); domainName != "" {
|
|
return slug + "@" + domainName
|
|
}
|
|
for _, candidate := range append([]domain.TenantDomain{}, domainTenant.Domains...) {
|
|
domainName := strings.ToLower(strings.TrimSpace(candidate.Domain))
|
|
if domainName != "" {
|
|
return slug + "@" + domainName
|
|
}
|
|
}
|
|
for _, candidate := range tenant.Domains {
|
|
domainName := strings.ToLower(strings.TrimSpace(candidate.Domain))
|
|
if domainName != "" {
|
|
return slug + "@" + domainName
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func worksmobileTenantMailDomain(tenant domain.Tenant) string {
|
|
envKey := strings.TrimSuffix(worksmobileTenantDomainIDEnvKey(tenant), "_DOMAIN_ID")
|
|
if domainName := strings.ToLower(strings.TrimSpace(os.Getenv("WORKS_DEFAULT_DOMAIN_" + envKey))); domainName != "" {
|
|
return domainName
|
|
}
|
|
if domainName := strings.ToLower(strings.TrimSpace(os.Getenv(envKey + "_MAIL_DOMAIN"))); domainName != "" {
|
|
return domainName
|
|
}
|
|
switch envKey {
|
|
case "SAMAN":
|
|
return "samaneng.com"
|
|
case "HANMAC":
|
|
return "hanmaceng.co.kr"
|
|
case "GPDTDC":
|
|
return "baroncs.co.kr"
|
|
case "HALLA":
|
|
return "hallasanup.com"
|
|
case "BARONGROUP":
|
|
return "brsw.kr"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func BuildWorksmobileUserPayload(user domain.User, tenant domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) {
|
|
return BuildWorksmobileUserPayloadForDomainTenant(user, tenant, tenant, rootConfig)
|
|
}
|
|
|
|
func BuildWorksmobileUserPayloadForDomainTenant(user domain.User, tenant domain.Tenant, _ domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) {
|
|
return BuildWorksmobileUserPayloadForDomainTenants(user, tenant, map[string]domain.Tenant{tenant.ID: tenant}, rootConfig)
|
|
}
|
|
|
|
func BuildWorksmobileUserPayloadForDomainTenants(user domain.User, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) {
|
|
return buildWorksmobileUserPayloadForDomainTenants(user, tenant, tenantByID, rootConfig, true)
|
|
}
|
|
|
|
func BuildWorksmobileUserPayloadForScopedDomainTenants(user domain.User, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap) (WorksmobileUserPayload, error) {
|
|
return buildWorksmobileUserPayloadForDomainTenants(user, tenant, tenantByID, rootConfig, false)
|
|
}
|
|
|
|
func buildWorksmobileUserPayloadForDomainTenants(user domain.User, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap, includeFallbackTenant bool) (WorksmobileUserPayload, error) {
|
|
if err := ValidateWorksmobileExternalKey(user.ID); err != nil {
|
|
return WorksmobileUserPayload{}, err
|
|
}
|
|
if tenant.ID == "" {
|
|
return WorksmobileUserPayload{}, errors.New("tenant is required")
|
|
}
|
|
if tenantByID == nil {
|
|
tenantByID = map[string]domain.Tenant{}
|
|
}
|
|
if includeFallbackTenant {
|
|
tenantByID[tenant.ID] = tenant
|
|
}
|
|
domainID, err := ResolveWorksmobileAccountDomainIDFromEmail(user.Email, tenant, rootConfig)
|
|
if err != nil {
|
|
return WorksmobileUserPayload{}, err
|
|
}
|
|
employeeNumber := metadataEmployeeNumber(user.Metadata)
|
|
organizations, task, err := buildWorksmobileUserOrganizations(user, tenant, tenantByID, rootConfig)
|
|
if err != nil {
|
|
return WorksmobileUserPayload{}, err
|
|
}
|
|
levelID, levelDomainID, err := worksmobileUserLevel(user, tenantByID, rootConfig)
|
|
if err != nil {
|
|
return WorksmobileUserPayload{}, err
|
|
}
|
|
if task == "" {
|
|
task = strings.TrimSpace(user.JobTitle)
|
|
}
|
|
payload := WorksmobileUserPayload{
|
|
DomainID: domainID,
|
|
Email: strings.TrimSpace(user.Email),
|
|
UserExternalKey: user.ID,
|
|
UserName: WorksmobileUserName{LastName: strings.TrimSpace(user.Name)},
|
|
CellPhone: domain.NormalizePhoneNumber(user.Phone),
|
|
EmployeeNumber: employeeNumber,
|
|
Locale: "ko_KR",
|
|
LevelID: levelID,
|
|
LevelDomainID: levelDomainID,
|
|
Task: task,
|
|
Organizations: organizations,
|
|
}
|
|
payload.AliasEmails = BuildWorksmobileAliasEmails(user, tenant)
|
|
return payload, nil
|
|
}
|
|
|
|
type worksmobileAppointment struct {
|
|
TenantID string
|
|
IsPrimary bool
|
|
IsManager bool
|
|
HasManager bool
|
|
Grade string
|
|
JobTitle string
|
|
PositionID string
|
|
Source string
|
|
}
|
|
|
|
func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap) ([]WorksmobileUserOrganization, string, error) {
|
|
appointments := worksmobileAppointmentsFromMetadata(user.Metadata)
|
|
if len(appointments) == 0 {
|
|
appointments = []worksmobileAppointment{{TenantID: tenant.ID, IsPrimary: true}}
|
|
} else if !worksmobileAppointmentsContainSyncableOrgUnit(appointments, tenantByID) && !worksmobileAppointmentsContainTenant(appointments, tenant.ID) {
|
|
appointments = append([]worksmobileAppointment{{
|
|
TenantID: tenant.ID,
|
|
IsPrimary: true,
|
|
Grade: strings.TrimSpace(user.Grade),
|
|
JobTitle: strings.TrimSpace(user.JobTitle),
|
|
PositionID: metadataString(user.Metadata, "worksmobilePositionId", "positionId", "position_id"),
|
|
}}, appointments...)
|
|
}
|
|
accountDomainTenant := worksmobileAccountDomainTenantFromEmail(user.Email, tenant, tenantByID)
|
|
accountDomainEnvKey := worksmobileTenantDomainIDEnvKey(accountDomainTenant)
|
|
if !worksmobileAppointmentsContainDomain(appointments, tenantByID, accountDomainEnvKey) && accountDomainTenant.ID != "" {
|
|
appointments = append([]worksmobileAppointment{{
|
|
TenantID: accountDomainTenant.ID,
|
|
IsPrimary: true,
|
|
Grade: strings.TrimSpace(user.Grade),
|
|
JobTitle: strings.TrimSpace(user.JobTitle),
|
|
PositionID: metadataString(user.Metadata, "worksmobilePositionId", "positionId", "position_id"),
|
|
}}, appointments...)
|
|
}
|
|
|
|
organizations := make([]WorksmobileUserOrganization, 0)
|
|
organizationIndexByDomainID := map[int64]int{}
|
|
seen := map[string]bool{}
|
|
task := ""
|
|
fallbackOrganizationIndex := -1
|
|
fallbackTask := ""
|
|
primaryOrganizationIndex := -1
|
|
primaryTask := ""
|
|
for _, appointment := range appointments {
|
|
if appointment.TenantID == "" || seen[appointment.TenantID] {
|
|
continue
|
|
}
|
|
appointmentTenant, ok := tenantByID[appointment.TenantID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if worksmobileTenantExcludedFromSync(appointmentTenant, tenantByID) {
|
|
seen[appointment.TenantID] = true
|
|
continue
|
|
}
|
|
if worksmobileShouldSkipEmailDomainRootAppointment(appointment, appointmentTenant, appointments, tenantByID) {
|
|
seen[appointment.TenantID] = true
|
|
continue
|
|
}
|
|
if isWorksmobileDomainRootTenant(appointmentTenant) {
|
|
if appointment.IsPrimary && strings.TrimSpace(appointment.JobTitle) != "" && primaryTask == "" {
|
|
primaryTask = strings.TrimSpace(appointment.JobTitle)
|
|
}
|
|
seen[appointment.TenantID] = true
|
|
continue
|
|
}
|
|
if err := ValidateWorksmobileExternalKey(appointmentTenant.ID); err != nil {
|
|
return nil, "", err
|
|
}
|
|
domainTenant := worksmobileDomainClassificationTenant(appointmentTenant, tenantByID)
|
|
domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
levelID, levelName := worksmobileOrganizationLevelForAppointment(appointment, tenantByID)
|
|
organizationIndex, organizationExists := organizationIndexByDomainID[domainID]
|
|
orgUnit := WorksmobileUserOrgUnit{
|
|
OrgUnitID: "externalKey:" + appointmentTenant.ID,
|
|
Primary: !organizationExists,
|
|
PositionID: appointment.PositionID,
|
|
}
|
|
if appointment.IsPrimary {
|
|
orgUnit.Primary = true
|
|
}
|
|
if appointment.HasManager {
|
|
isManager := appointment.IsManager
|
|
orgUnit.IsManager = &isManager
|
|
}
|
|
if organizationExists {
|
|
if appointment.IsPrimary {
|
|
for index := range organizations[organizationIndex].OrgUnits {
|
|
organizations[organizationIndex].OrgUnits[index].Primary = false
|
|
}
|
|
}
|
|
worksmobileApplyOrganizationLevel(&organizations[organizationIndex], levelID, levelName, appointment.IsPrimary)
|
|
organizations[organizationIndex].OrgUnits = append(organizations[organizationIndex].OrgUnits, orgUnit)
|
|
} else {
|
|
organizationIndexByDomainID[domainID] = len(organizations)
|
|
organizationIndex = len(organizations)
|
|
organizations = append(organizations, WorksmobileUserOrganization{
|
|
DomainID: domainID,
|
|
Email: worksmobileOrganizationEmail(user, domainTenant),
|
|
LevelID: levelID,
|
|
LevelName: levelName,
|
|
OrgUnits: []WorksmobileUserOrgUnit{orgUnit},
|
|
})
|
|
}
|
|
if fallbackOrganizationIndex == -1 {
|
|
fallbackOrganizationIndex = organizationIndex
|
|
}
|
|
if fallbackTask == "" && strings.TrimSpace(appointment.JobTitle) != "" {
|
|
fallbackTask = strings.TrimSpace(appointment.JobTitle)
|
|
}
|
|
if appointment.IsPrimary && primaryOrganizationIndex == -1 {
|
|
primaryOrganizationIndex = organizationIndex
|
|
}
|
|
if appointment.IsPrimary && primaryTask == "" && strings.TrimSpace(appointment.JobTitle) != "" {
|
|
primaryTask = strings.TrimSpace(appointment.JobTitle)
|
|
}
|
|
seen[appointment.TenantID] = true
|
|
}
|
|
if len(organizations) == 0 {
|
|
if primaryTask != "" {
|
|
task = primaryTask
|
|
} else {
|
|
task = fallbackTask
|
|
}
|
|
return nil, task, nil
|
|
}
|
|
selectedOrganizationIndex := primaryOrganizationIndex
|
|
if selectedOrganizationIndex == -1 {
|
|
selectedOrganizationIndex = fallbackOrganizationIndex
|
|
}
|
|
if selectedOrganizationIndex == -1 {
|
|
selectedOrganizationIndex = 0
|
|
}
|
|
for index := range organizations {
|
|
organizations[index].Primary = index == selectedOrganizationIndex
|
|
}
|
|
if len(organizations[selectedOrganizationIndex].OrgUnits) > 0 && !worksmobileOrgUnitsHavePrimary(organizations[selectedOrganizationIndex].OrgUnits) {
|
|
organizations[selectedOrganizationIndex].OrgUnits[0].Primary = true
|
|
}
|
|
if primaryTask != "" {
|
|
task = primaryTask
|
|
} else {
|
|
task = fallbackTask
|
|
}
|
|
sortWorksmobileOrganizations(organizations)
|
|
return organizations, task, nil
|
|
}
|
|
|
|
func worksmobileOrganizationLevelForAppointment(appointment worksmobileAppointment, tenantByID map[string]domain.Tenant) (string, string) {
|
|
levelID := worksmobileLevelIDForTenant(appointment.Grade, appointment.TenantID, tenantByID)
|
|
if levelID == "" {
|
|
return "", ""
|
|
}
|
|
if isWorksmobileExternalKeyLevelID(levelID) {
|
|
return levelID, WorksmobileLevelDisplayNameForIdentifier(levelID)
|
|
}
|
|
return "", levelID
|
|
}
|
|
|
|
func worksmobileApplyOrganizationLevel(organization *WorksmobileUserOrganization, levelID, levelName string, prefer bool) {
|
|
if organization == nil || (strings.TrimSpace(levelID) == "" && strings.TrimSpace(levelName) == "") {
|
|
return
|
|
}
|
|
if (strings.TrimSpace(organization.LevelID) == "" && strings.TrimSpace(organization.LevelName) == "") || prefer {
|
|
organization.LevelID = levelID
|
|
organization.LevelName = levelName
|
|
}
|
|
}
|
|
|
|
func worksmobileAppointmentsContainTenant(appointments []worksmobileAppointment, tenantID string) bool {
|
|
tenantID = strings.TrimSpace(tenantID)
|
|
if tenantID == "" {
|
|
return false
|
|
}
|
|
for _, appointment := range appointments {
|
|
if strings.TrimSpace(appointment.TenantID) == tenantID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func worksmobileAppointmentsContainSyncableOrgUnit(appointments []worksmobileAppointment, tenantByID map[string]domain.Tenant) bool {
|
|
for _, appointment := range appointments {
|
|
tenant, ok := tenantByID[strings.TrimSpace(appointment.TenantID)]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if worksmobileTenantExcludedFromSync(tenant, tenantByID) {
|
|
continue
|
|
}
|
|
if isWorksmobileDomainRootTenant(tenant) {
|
|
continue
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func worksmobileAppointmentsHavePrimary(appointments []worksmobileAppointment) bool {
|
|
for _, appointment := range appointments {
|
|
if appointment.IsPrimary {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func worksmobileAppointmentsContainDomain(appointments []worksmobileAppointment, tenantByID map[string]domain.Tenant, envKey string) bool {
|
|
for _, appointment := range appointments {
|
|
tenant, ok := tenantByID[appointment.TenantID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if worksmobileTenantExcludedFromSync(tenant, tenantByID) {
|
|
continue
|
|
}
|
|
domainTenant := worksmobileDomainClassificationTenant(tenant, tenantByID)
|
|
if worksmobileTenantDomainIDEnvKey(domainTenant) == envKey {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func worksmobileTenantExcludedFromSync(tenant domain.Tenant, tenantByID map[string]domain.Tenant) bool {
|
|
visited := map[string]bool{}
|
|
current := tenant
|
|
for {
|
|
if WorksmobileExcluded(current.Config) {
|
|
return true
|
|
}
|
|
parentID := worksmobileTenantParentID(current)
|
|
if parentID == "" || visited[parentID] {
|
|
return false
|
|
}
|
|
visited[parentID] = true
|
|
parent, ok := tenantByID[parentID]
|
|
if !ok {
|
|
return false
|
|
}
|
|
current = parent
|
|
}
|
|
}
|
|
|
|
func worksmobileShouldSkipEmailDomainRootAppointment(appointment worksmobileAppointment, tenant domain.Tenant, appointments []worksmobileAppointment, tenantByID map[string]domain.Tenant) bool {
|
|
if strings.TrimSpace(appointment.Source) != "email_domain" || !isWorksmobileDomainRootTenant(tenant) {
|
|
return false
|
|
}
|
|
envKey := worksmobileTenantDomainIDEnvKey(tenant)
|
|
for _, candidate := range appointments {
|
|
if strings.TrimSpace(candidate.TenantID) == "" || strings.TrimSpace(candidate.TenantID) == tenant.ID {
|
|
continue
|
|
}
|
|
candidateTenant, ok := tenantByID[candidate.TenantID]
|
|
if !ok || isWorksmobileDomainRootTenant(candidateTenant) {
|
|
continue
|
|
}
|
|
if worksmobileTenantDomainIDEnvKey(worksmobileDomainClassificationTenant(candidateTenant, tenantByID)) == envKey {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func worksmobileOrganizationsHavePrimary(organizations []WorksmobileUserOrganization) bool {
|
|
for _, organization := range organizations {
|
|
if organization.Primary {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func worksmobileOrgUnitsHavePrimary(orgUnits []WorksmobileUserOrgUnit) bool {
|
|
for _, orgUnit := range orgUnits {
|
|
if orgUnit.Primary {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func worksmobileAppointmentsFromMetadata(metadata domain.JSONMap) []worksmobileAppointment {
|
|
rawAppointments, ok := metadata["additionalAppointments"].([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
appointments := make([]worksmobileAppointment, 0, len(rawAppointments))
|
|
for _, raw := range rawAppointments {
|
|
item, ok := raw.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
appointment := worksmobileAppointment{
|
|
TenantID: metadataString(domain.JSONMap(item), "tenantId", "tenant_id"),
|
|
IsPrimary: metadataBool(domain.JSONMap(item), "isPrimary", "primary"),
|
|
Grade: metadataString(domain.JSONMap(item), "grade"),
|
|
JobTitle: metadataString(domain.JSONMap(item), "jobTitle", "job_title", "task"),
|
|
PositionID: metadataString(domain.JSONMap(item), "worksmobilePositionId", "positionId", "position_id"),
|
|
Source: metadataString(domain.JSONMap(item), "assignmentSource", "source"),
|
|
}
|
|
if isManager, ok := metadataOptionalBool(domain.JSONMap(item), "isManager", "lead", "isLead"); ok {
|
|
appointment.IsManager = isManager
|
|
appointment.HasManager = true
|
|
}
|
|
appointments = append(appointments, appointment)
|
|
}
|
|
return appointments
|
|
}
|
|
|
|
func worksmobileUserGrade(user domain.User) string {
|
|
grade, _ := worksmobileUserGradeWithTenant(user)
|
|
return grade
|
|
}
|
|
|
|
func worksmobileUserLevel(user domain.User, tenantByID map[string]domain.Tenant, rootConfig domain.JSONMap) (string, int64, error) {
|
|
grade, tenantID := worksmobileUserGradeWithTenant(user)
|
|
grade = worksmobileLevelIDForTenant(grade, tenantID, tenantByID)
|
|
if grade == "" {
|
|
return "", 0, nil
|
|
}
|
|
tenant, ok := tenantByID[strings.TrimSpace(tenantID)]
|
|
if !ok {
|
|
return grade, 0, nil
|
|
}
|
|
domainTenant := worksmobileDomainClassificationTenant(tenant, tenantByID)
|
|
domainID, err := ResolveWorksmobileDomainIDFromTenant(domainTenant, rootConfig)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
return grade, domainID, nil
|
|
}
|
|
|
|
func worksmobileUserGradeWithTenant(user domain.User) (string, string) {
|
|
appointments := worksmobileAppointmentsFromMetadata(user.Metadata)
|
|
for _, appointment := range appointments {
|
|
if appointment.IsPrimary && strings.TrimSpace(appointment.Grade) != "" {
|
|
return strings.TrimSpace(appointment.Grade), strings.TrimSpace(appointment.TenantID)
|
|
}
|
|
}
|
|
for _, appointment := range appointments {
|
|
if strings.TrimSpace(appointment.Grade) != "" {
|
|
return strings.TrimSpace(appointment.Grade), strings.TrimSpace(appointment.TenantID)
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
const worksmobileExternalKeyLevelIDPrefix = "externalKey:"
|
|
|
|
type worksmobileGPDTDCLevelMapping struct {
|
|
DisplayName string
|
|
ExternalKey string
|
|
Aliases []string
|
|
}
|
|
|
|
var worksmobileGPDTDCLevelMappings = []worksmobileGPDTDCLevelMapping{
|
|
{DisplayName: "사장", ExternalKey: "pres", Aliases: []string{"사장"}},
|
|
{DisplayName: "부사장", ExternalKey: "vp", Aliases: []string{"부사장"}},
|
|
{DisplayName: "수석 연구원", ExternalKey: "prin", Aliases: []string{"수석", "수석연구원", "수석 연구원"}},
|
|
{DisplayName: "책임 연구원", ExternalKey: "lead", Aliases: []string{"책임", "책임연구원", "책임 연구원"}},
|
|
{DisplayName: "선임 연구원", ExternalKey: "sen", Aliases: []string{"선임", "선임연구원", "선임 연구원"}},
|
|
{DisplayName: "연구원", ExternalKey: "res", Aliases: []string{"연구원"}},
|
|
}
|
|
|
|
func normalizeWorksmobileGradeForTenant(grade, tenantID string, tenantByID map[string]domain.Tenant) string {
|
|
grade = strings.TrimSpace(grade)
|
|
if grade == "" {
|
|
return ""
|
|
}
|
|
if directorLevel := normalizeWorksmobileDirectorLevelName(grade); directorLevel != "" {
|
|
return directorLevel
|
|
}
|
|
if !worksmobileTenantIsGPDTDCDescendant(tenantID, tenantByID) {
|
|
return grade
|
|
}
|
|
if level, ok := worksmobileGPDTDCLevelMappingForGrade(grade); ok {
|
|
return level.DisplayName
|
|
}
|
|
return grade
|
|
}
|
|
|
|
func normalizeWorksmobileDirectorLevelName(grade string) string {
|
|
switch strings.ReplaceAll(strings.TrimSpace(grade), " ", "") {
|
|
case "상무":
|
|
return "상무이사"
|
|
case "전무":
|
|
return "전무이사"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func worksmobileLevelIDForTenant(grade, tenantID string, tenantByID map[string]domain.Tenant) string {
|
|
displayName := normalizeWorksmobileGradeForTenant(grade, tenantID, tenantByID)
|
|
if displayName == "" || !worksmobileTenantIsGPDTDCDescendant(tenantID, tenantByID) {
|
|
return displayName
|
|
}
|
|
if level, ok := worksmobileGPDTDCLevelMappingForGrade(displayName); ok {
|
|
return worksmobileExternalKeyLevelID(level.ExternalKey)
|
|
}
|
|
return displayName
|
|
}
|
|
|
|
func worksmobileExternalKeyLevelID(externalKey string) string {
|
|
externalKey = strings.TrimSpace(externalKey)
|
|
if externalKey == "" {
|
|
return ""
|
|
}
|
|
if strings.HasPrefix(externalKey, worksmobileExternalKeyLevelIDPrefix) {
|
|
return externalKey
|
|
}
|
|
return worksmobileExternalKeyLevelIDPrefix + externalKey
|
|
}
|
|
|
|
func isWorksmobileExternalKeyLevelID(levelID string) bool {
|
|
return strings.HasPrefix(strings.TrimSpace(levelID), worksmobileExternalKeyLevelIDPrefix)
|
|
}
|
|
|
|
func worksmobileGPDTDCLevelMappingForGrade(grade string) (worksmobileGPDTDCLevelMapping, bool) {
|
|
compact := strings.ReplaceAll(strings.TrimSpace(grade), " ", "")
|
|
if compact == "" {
|
|
return worksmobileGPDTDCLevelMapping{}, false
|
|
}
|
|
for _, level := range worksmobileGPDTDCLevelMappings {
|
|
for _, alias := range level.Aliases {
|
|
if strings.ReplaceAll(strings.TrimSpace(alias), " ", "") == compact {
|
|
return level, true
|
|
}
|
|
}
|
|
}
|
|
return worksmobileGPDTDCLevelMapping{}, false
|
|
}
|
|
|
|
func worksmobileGPDTDCLevelMappingForExternalKey(levelID string) (worksmobileGPDTDCLevelMapping, bool) {
|
|
key := strings.TrimSpace(levelID)
|
|
key = strings.TrimPrefix(key, worksmobileExternalKeyLevelIDPrefix)
|
|
if key == "" {
|
|
return worksmobileGPDTDCLevelMapping{}, false
|
|
}
|
|
for _, level := range worksmobileGPDTDCLevelMappings {
|
|
if level.ExternalKey == key {
|
|
return level, true
|
|
}
|
|
}
|
|
return worksmobileGPDTDCLevelMapping{}, false
|
|
}
|
|
|
|
func WorksmobileLevelDisplayNameForIdentifier(levelID string) string {
|
|
levelID = strings.TrimSpace(levelID)
|
|
if levelID == "" {
|
|
return ""
|
|
}
|
|
if level, ok := worksmobileGPDTDCLevelMappingForExternalKey(levelID); ok {
|
|
return level.DisplayName
|
|
}
|
|
return levelID
|
|
}
|
|
|
|
func WorksmobileLevelIdentifierMatchesRemote(expectedLevelID, remoteLevelID, remoteLevelName string) bool {
|
|
expectedLevelID = strings.TrimSpace(expectedLevelID)
|
|
remoteLevelID = strings.TrimSpace(remoteLevelID)
|
|
remoteLevelName = strings.TrimSpace(remoteLevelName)
|
|
if expectedLevelID == "" {
|
|
return remoteLevelID == "" && remoteLevelName == ""
|
|
}
|
|
if remoteLevelID == expectedLevelID || remoteLevelName == expectedLevelID {
|
|
return true
|
|
}
|
|
if worksmobileDirectorLevelNamesEquivalent(expectedLevelID, remoteLevelName) {
|
|
return true
|
|
}
|
|
if level, ok := worksmobileGPDTDCLevelMappingForExternalKey(expectedLevelID); ok {
|
|
if remoteLevelID == level.ExternalKey || remoteLevelName == level.DisplayName {
|
|
return true
|
|
}
|
|
for _, alias := range level.Aliases {
|
|
if strings.TrimSpace(alias) == remoteLevelName {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func worksmobileDirectorLevelNamesEquivalent(expectedLevelName, remoteLevelName string) bool {
|
|
expectedLevelName = strings.ReplaceAll(strings.TrimSpace(expectedLevelName), " ", "")
|
|
remoteLevelName = strings.ReplaceAll(strings.TrimSpace(remoteLevelName), " ", "")
|
|
if expectedLevelName == "" || remoteLevelName == "" {
|
|
return false
|
|
}
|
|
return (expectedLevelName == "상무이사" && remoteLevelName == "상무") ||
|
|
(expectedLevelName == "상무" && remoteLevelName == "상무이사") ||
|
|
(expectedLevelName == "전무이사" && remoteLevelName == "전무") ||
|
|
(expectedLevelName == "전무" && remoteLevelName == "전무이사")
|
|
}
|
|
|
|
func sortWorksmobileOrganizations(organizations []WorksmobileUserOrganization) {
|
|
sort.SliceStable(organizations, func(i, j int) bool {
|
|
if organizations[i].Primary != organizations[j].Primary {
|
|
return organizations[i].Primary
|
|
}
|
|
left := ""
|
|
right := ""
|
|
if len(organizations[i].OrgUnits) > 0 {
|
|
left = organizations[i].OrgUnits[0].OrgUnitID
|
|
}
|
|
if len(organizations[j].OrgUnits) > 0 {
|
|
right = organizations[j].OrgUnits[0].OrgUnitID
|
|
}
|
|
return left < right
|
|
})
|
|
}
|
|
|
|
func BuildWorksmobileAliasEmails(user domain.User, tenant domain.Tenant) []string {
|
|
candidates := make([]string, 0)
|
|
for _, key := range []string{
|
|
"aliasEmails",
|
|
"alias_emails",
|
|
"worksmobileAliasEmails",
|
|
"sub_email",
|
|
"secondary_email",
|
|
"secondary_emails",
|
|
"additional_email",
|
|
"additional_emails",
|
|
"naverworks_sub_email",
|
|
} {
|
|
candidates = append(candidates, metadataStringList(user.Metadata, key)...)
|
|
}
|
|
employeeNumber := metadataEmployeeNumber(user.Metadata)
|
|
if isHanmacWorksmobileTenant(tenant) && employeeNumber != "" {
|
|
candidates = append(candidates, employeeNumber+"@hanmaceng.co.kr")
|
|
}
|
|
return normalizeWorksmobileAliasEmails(user.Email, candidates)
|
|
}
|
|
|
|
func normalizeWorksmobileAliasEmails(primaryEmail string, candidates []string) []string {
|
|
result := make([]string, 0, len(candidates))
|
|
seen := map[string]bool{}
|
|
primary := strings.ToLower(strings.TrimSpace(primaryEmail))
|
|
for _, candidate := range candidates {
|
|
normalized := strings.ToLower(strings.TrimSpace(candidate))
|
|
if normalized == "" || normalized == primary || seen[normalized] {
|
|
continue
|
|
}
|
|
if _, err := mail.ParseAddress(normalized); err != nil {
|
|
continue
|
|
}
|
|
seen[normalized] = true
|
|
result = append(result, normalized)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func ValidateWorksmobileAliasEmails(primaryEmail string, aliasEmails []string, existingEmails map[string]string) error {
|
|
seen := map[string]string{strings.ToLower(strings.TrimSpace(primaryEmail)): primaryEmail}
|
|
|
|
for _, aliasEmail := range aliasEmails {
|
|
normalized := strings.ToLower(strings.TrimSpace(aliasEmail))
|
|
if _, err := mail.ParseAddress(normalized); err != nil {
|
|
return err
|
|
}
|
|
if previous, ok := seen[normalized]; ok {
|
|
return fmt.Errorf("worksmobile alias email duplicates: %s and %s", previous, aliasEmail)
|
|
}
|
|
if owner, ok := existingEmails[normalized]; ok {
|
|
return fmt.Errorf("worksmobile alias email %s는 이미 사용 중입니다: %s", normalized, owner)
|
|
}
|
|
seen[normalized] = aliasEmail
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func GenerateWorksmobileInitialPassword() string {
|
|
digits := "0123456789"
|
|
letters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
symbols := "!@#$%"
|
|
all := digits + letters + symbols
|
|
|
|
password := []byte{
|
|
randomChar(digits),
|
|
randomChar(letters),
|
|
randomChar(symbols),
|
|
}
|
|
for len(password) < 16 {
|
|
password = append(password, randomChar(all))
|
|
}
|
|
shuffleBytes(password)
|
|
return string(password)
|
|
}
|
|
|
|
func randomChar(chars string) byte {
|
|
if chars == "" {
|
|
return 'x'
|
|
}
|
|
index, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
|
if err != nil {
|
|
return chars[0]
|
|
}
|
|
return chars[index.Int64()]
|
|
}
|
|
|
|
func shuffleBytes(values []byte) {
|
|
for i := len(values) - 1; i > 0; i-- {
|
|
j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
values[i], values[j.Int64()] = values[j.Int64()], values[i]
|
|
}
|
|
}
|
|
|
|
func WorksmobileUserStatusAction(status string) string {
|
|
normalized := domain.NormalizeUserStatus(status)
|
|
if domain.IsWorksDeprovisionUserStatus(normalized) {
|
|
return domain.WorksmobileActionDelete
|
|
}
|
|
switch normalized {
|
|
case domain.UserStatusSuspended:
|
|
return WorksmobileUserActionSuspend
|
|
default:
|
|
return WorksmobileUserActionUpsert
|
|
}
|
|
}
|
|
|
|
func ValidateWorksmobileExternalKey(value string) error {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return errors.New("external key is required")
|
|
}
|
|
if strings.ContainsAny(value, `%\#/?`) {
|
|
return fmt.Errorf("external key contains unsupported character: %s", value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ResolveWorksmobileDomainIDFromTenant(tenant domain.Tenant, _ domain.JSONMap) (int64, error) {
|
|
envKey := worksmobileTenantDomainIDEnvKey(tenant)
|
|
if domainID, ok := worksmobileDomainIDFromEnv(envKey); ok {
|
|
return domainID, nil
|
|
}
|
|
return 0, fmt.Errorf("worksmobile domain id env is missing for tenant: %s", envKey)
|
|
}
|
|
|
|
func ResolveWorksmobileAccountDomainIDFromEmail(email string, fallbackTenant domain.Tenant, rootConfig domain.JSONMap) (int64, error) {
|
|
switch worksmobileEmailDomainName(email) {
|
|
case "samaneng.com":
|
|
if domainID, ok := worksmobileDomainIDFromEnv("SAMAN_DOMAIN_ID"); ok {
|
|
return domainID, nil
|
|
}
|
|
case "hanmaceng.co.kr":
|
|
if domainID, ok := worksmobileDomainIDFromEnv("HANMAC_DOMAIN_ID"); ok {
|
|
return domainID, nil
|
|
}
|
|
case "baroncs.co.kr":
|
|
if domainID, ok := worksmobileDomainIDFromEnv("GPDTDC_DOMAIN_ID"); ok {
|
|
return domainID, nil
|
|
}
|
|
case "hallasanup.com":
|
|
if domainID, ok := worksmobileDomainIDFromEnv("HALLA_DOMAIN_ID"); ok {
|
|
return domainID, nil
|
|
}
|
|
case "brsw.kr":
|
|
if domainID, ok := worksmobileDomainIDFromEnv("BARONGROUP_DOMAIN_ID"); ok {
|
|
return domainID, nil
|
|
}
|
|
}
|
|
return ResolveWorksmobileDomainIDFromTenant(fallbackTenant, rootConfig)
|
|
}
|
|
|
|
func worksmobileAccountDomainTenantFromEmail(email string, fallbackTenant domain.Tenant, tenantByID map[string]domain.Tenant) domain.Tenant {
|
|
envKey := worksmobileDomainIDEnvKeyFromEmail(email)
|
|
for _, tenant := range tenantByID {
|
|
if isWorksmobileDomainRootTenant(tenant) && worksmobileTenantDomainIDEnvKey(tenant) == envKey {
|
|
return tenant
|
|
}
|
|
}
|
|
for _, tenant := range tenantByID {
|
|
if worksmobileTenantDomainIDEnvKey(tenant) == envKey {
|
|
return worksmobileDomainClassificationTenant(tenant, tenantByID)
|
|
}
|
|
}
|
|
return worksmobileDomainClassificationTenant(fallbackTenant, tenantByID)
|
|
}
|
|
|
|
func worksmobileDomainIDEnvKeyFromEmail(email string) string {
|
|
switch worksmobileEmailDomainName(email) {
|
|
case "samaneng.com":
|
|
return "SAMAN_DOMAIN_ID"
|
|
case "hanmaceng.co.kr":
|
|
return "HANMAC_DOMAIN_ID"
|
|
case "baroncs.co.kr":
|
|
return "GPDTDC_DOMAIN_ID"
|
|
case "hallasanup.com":
|
|
return "HALLA_DOMAIN_ID"
|
|
case "brsw.kr":
|
|
return "BARONGROUP_DOMAIN_ID"
|
|
default:
|
|
return worksmobileTenantDomainIDEnvKey(domain.Tenant{})
|
|
}
|
|
}
|
|
|
|
func worksmobileEmailDomainName(email string) string {
|
|
address, err := mail.ParseAddress(strings.TrimSpace(email))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
parts := strings.Split(address.Address, "@")
|
|
if len(parts) != 2 {
|
|
return ""
|
|
}
|
|
return strings.ToLower(strings.TrimSpace(parts[1]))
|
|
}
|
|
|
|
func worksmobileOrganizationEmail(user domain.User, domainTenant domain.Tenant) string {
|
|
domainName := worksmobileTenantMailDomain(domainTenant)
|
|
if domainName == "" {
|
|
return ""
|
|
}
|
|
primaryEmail := strings.ToLower(strings.TrimSpace(user.Email))
|
|
if worksmobileEmailDomainName(primaryEmail) == domainName {
|
|
return primaryEmail
|
|
}
|
|
for _, alias := range BuildWorksmobileAliasEmails(user, domainTenant) {
|
|
if worksmobileEmailDomainName(alias) == domainName {
|
|
return alias
|
|
}
|
|
}
|
|
localPart, err := domain.ExtractNormalizedEmailLocalPart(primaryEmail)
|
|
if err != nil || localPart == "" {
|
|
return ""
|
|
}
|
|
return localPart + "@" + domainName
|
|
}
|
|
|
|
func worksmobileTenantDomainIDEnvKey(tenant domain.Tenant) string {
|
|
if tenantHasDomain(tenant, "samaneng.com") || tenantMatchesAny(tenant, "saman", "삼안") {
|
|
return "SAMAN_DOMAIN_ID"
|
|
}
|
|
if isHanmacWorksmobileTenant(tenant) {
|
|
return "HANMAC_DOMAIN_ID"
|
|
}
|
|
if tenantMatchesAny(tenant, "gpdtdc", "총괄", "기술개발센터", "기술개발") {
|
|
return "GPDTDC_DOMAIN_ID"
|
|
}
|
|
if isHallaWorksmobileTenant(tenant) {
|
|
return "HALLA_DOMAIN_ID"
|
|
}
|
|
return "BARONGROUP_DOMAIN_ID"
|
|
}
|
|
|
|
func worksmobileDomainIDFromEnv(key string) (int64, bool) {
|
|
if key == "" {
|
|
return 0, false
|
|
}
|
|
id, ok := parseDomainID(os.Getenv(key))
|
|
return id, ok
|
|
}
|
|
|
|
type worksmobileDomainEnvMapping struct {
|
|
Key string
|
|
Label string
|
|
}
|
|
|
|
func worksmobileDomainEnvMappings() []worksmobileDomainEnvMapping {
|
|
return []worksmobileDomainEnvMapping{
|
|
{Key: "SAMAN_DOMAIN_ID", Label: "삼안"},
|
|
{Key: "HANMAC_DOMAIN_ID", Label: "한맥기술"},
|
|
{Key: "GPDTDC_DOMAIN_ID", Label: "총괄기획&기술개발센터"},
|
|
{Key: "HALLA_DOMAIN_ID", Label: "한라산업개발"},
|
|
{Key: "BARONGROUP_DOMAIN_ID", Label: "바론그룹"},
|
|
}
|
|
}
|
|
|
|
func WorksmobileDomainIDsFromEnv() []int64 {
|
|
mappings := worksmobileDomainEnvMappings()
|
|
result := make([]int64, 0, len(mappings))
|
|
seen := map[int64]bool{}
|
|
for _, mapping := range mappings {
|
|
if id, ok := worksmobileDomainIDFromEnv(mapping.Key); ok && !seen[id] {
|
|
seen[id] = true
|
|
result = append(result, id)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func WorksmobileDomainLabelForID(domainID int64) string {
|
|
for _, mapping := range worksmobileDomainEnvMappings() {
|
|
if id, ok := worksmobileDomainIDFromEnv(mapping.Key); ok && id == domainID {
|
|
return mapping.Label
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isHanmacWorksmobileTenant(tenant domain.Tenant) bool {
|
|
return tenantHasDomain(tenant, "hanmaceng.co.kr") || tenantMatchesAny(tenant, "hanmac", "한맥")
|
|
}
|
|
|
|
func isHallaWorksmobileTenant(tenant domain.Tenant) bool {
|
|
return tenantHasDomain(tenant, "hallasanup.com") || tenantMatchesAny(tenant, "halla", "hanlla", "한라산업개발")
|
|
}
|
|
|
|
func tenantHasDomain(tenant domain.Tenant, domainName string) bool {
|
|
domainName = strings.ToLower(strings.TrimSpace(domainName))
|
|
for _, d := range tenant.Domains {
|
|
if strings.EqualFold(strings.TrimSpace(d.Domain), domainName) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func tenantMatchesAny(tenant domain.Tenant, needles ...string) bool {
|
|
haystack := strings.ToLower(strings.TrimSpace(tenant.Slug + " " + tenant.Name))
|
|
for _, needle := range needles {
|
|
if strings.Contains(haystack, strings.ToLower(strings.TrimSpace(needle))) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func WorksmobileEnabled(rootConfig domain.JSONMap) bool {
|
|
rawWorksmobile, ok := rootConfig["worksmobile"].(map[string]any)
|
|
if !ok {
|
|
if raw, ok := rootConfig["worksmobile"].(domain.JSONMap); ok {
|
|
rawWorksmobile = map[string]any(raw)
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
enabled, _ := rawWorksmobile["enabled"].(bool)
|
|
return enabled
|
|
}
|
|
|
|
func WorksmobileDomainMappings(rootConfig domain.JSONMap) map[string]int64 {
|
|
result := map[string]int64{}
|
|
rawWorksmobile, ok := rootConfig["worksmobile"].(map[string]any)
|
|
if !ok {
|
|
if raw, ok := rootConfig["worksmobile"].(domain.JSONMap); ok {
|
|
rawWorksmobile = map[string]any(raw)
|
|
} else {
|
|
return result
|
|
}
|
|
}
|
|
rawMappings, ok := rawWorksmobile["domainMappings"].(map[string]any)
|
|
if !ok {
|
|
if raw, ok := rawWorksmobile["domainMappings"].(domain.JSONMap); ok {
|
|
rawMappings = map[string]any(raw)
|
|
} else {
|
|
return result
|
|
}
|
|
}
|
|
for key, raw := range rawMappings {
|
|
if id, ok := parseDomainID(raw); ok {
|
|
result[strings.ToLower(strings.TrimSpace(key))] = id
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parseDomainID(raw any) (int64, bool) {
|
|
switch value := raw.(type) {
|
|
case int:
|
|
return int64(value), value > 0
|
|
case int64:
|
|
return value, value > 0
|
|
case float64:
|
|
id := int64(value)
|
|
return id, id > 0
|
|
case string:
|
|
id, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
|
return id, err == nil && id > 0
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func metadataString(metadata domain.JSONMap, keys ...string) string {
|
|
for _, key := range keys {
|
|
if value, ok := metadata[key]; ok {
|
|
switch v := value.(type) {
|
|
case string:
|
|
return strings.TrimSpace(v)
|
|
default:
|
|
return strings.TrimSpace(fmt.Sprint(v))
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func metadataEmployeeNumber(metadata domain.JSONMap) string {
|
|
for _, key := range []string{"employee_id", "employeeNumber", "employee_number"} {
|
|
value, ok := metadata[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if normalized := normalizeMetadataEmployeeNumber(value); normalized != "" {
|
|
return normalized
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func normalizeMetadataEmployeeNumber(value any) string {
|
|
switch v := value.(type) {
|
|
case string:
|
|
return strings.TrimSpace(v)
|
|
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
|
return strings.TrimSpace(fmt.Sprint(v))
|
|
case map[string]any:
|
|
return normalizeMetadataCharacterMap(v)
|
|
case domain.JSONMap:
|
|
return normalizeMetadataCharacterMap(map[string]any(v))
|
|
case map[string]string:
|
|
converted := make(map[string]any, len(v))
|
|
for key, value := range v {
|
|
converted[key] = value
|
|
}
|
|
return normalizeMetadataCharacterMap(converted)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func normalizeMetadataCharacterMap(value map[string]any) string {
|
|
type characterEntry struct {
|
|
index int
|
|
value string
|
|
}
|
|
entries := make([]characterEntry, 0, len(value))
|
|
for key, raw := range value {
|
|
index, err := strconv.Atoi(key)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
part, ok := raw.(string)
|
|
if !ok || part == "" {
|
|
return ""
|
|
}
|
|
entries = append(entries, characterEntry{index: index, value: part})
|
|
}
|
|
if len(entries) == 0 {
|
|
return ""
|
|
}
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].index < entries[j].index
|
|
})
|
|
var builder strings.Builder
|
|
for _, entry := range entries {
|
|
builder.WriteString(entry.value)
|
|
}
|
|
return strings.TrimSpace(builder.String())
|
|
}
|
|
|
|
func metadataBool(metadata domain.JSONMap, keys ...string) bool {
|
|
value, _ := metadataOptionalBool(metadata, keys...)
|
|
return value
|
|
}
|
|
|
|
func metadataOptionalBool(metadata domain.JSONMap, keys ...string) (bool, bool) {
|
|
for _, key := range keys {
|
|
value, ok := metadata[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch v := value.(type) {
|
|
case bool:
|
|
return v, true
|
|
case string:
|
|
normalized := strings.ToLower(strings.TrimSpace(v))
|
|
if normalized == "true" || normalized == "1" || normalized == "yes" {
|
|
return true, true
|
|
}
|
|
if normalized == "false" || normalized == "0" || normalized == "no" {
|
|
return false, true
|
|
}
|
|
case int:
|
|
return v != 0, true
|
|
case float64:
|
|
return v != 0, true
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func metadataStringList(metadata domain.JSONMap, keys ...string) []string {
|
|
for _, key := range keys {
|
|
value, ok := metadata[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch v := value.(type) {
|
|
case []string:
|
|
return splitWorksmobileAliasValues(v)
|
|
case []any:
|
|
values := make([]string, 0, len(v))
|
|
for _, item := range v {
|
|
values = append(values, strings.TrimSpace(fmt.Sprint(item)))
|
|
}
|
|
return splitWorksmobileAliasValues(values)
|
|
case string:
|
|
return splitWorksmobileAliasValues([]string{v})
|
|
default:
|
|
return splitWorksmobileAliasValues([]string{fmt.Sprint(v)})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func splitWorksmobileAliasValues(values []string) []string {
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
fields := strings.FieldsFunc(value, func(r rune) bool {
|
|
return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t'
|
|
})
|
|
for _, field := range fields {
|
|
if trimmed := strings.TrimSpace(field); trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|