1
0
forked from baron/baron-sso

chore: snapshot local state before dev merge

This commit is contained in:
2026-06-17 21:25:42 +09:00
parent b2808759d2
commit 49560e8a8c
107 changed files with 8958 additions and 939 deletions

View File

@@ -38,6 +38,8 @@ type WorksmobileUserPayload struct {
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"`
@@ -70,6 +72,8 @@ func (p WorksmobileUserPayload) MarshalJSON() ([]byte, error) {
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"`
@@ -90,22 +94,75 @@ func (p WorksmobileUserPayload) MarshalJSON() ([]byte, error) {
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"`
OrgUnits []WorksmobileUserOrgUnit `json:"orgUnits"`
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 {
@@ -231,6 +288,10 @@ func buildWorksmobileUserPayloadForDomainTenants(user domain.User, tenant domain
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)
}
@@ -242,6 +303,8 @@ func buildWorksmobileUserPayloadForDomainTenants(user domain.User, tenant domain
CellPhone: domain.NormalizePhoneNumber(user.Phone),
EmployeeNumber: employeeNumber,
Locale: "ko_KR",
LevelID: levelID,
LevelDomainID: levelDomainID,
Task: task,
Organizations: organizations,
}
@@ -254,6 +317,7 @@ type worksmobileAppointment struct {
IsPrimary bool
IsManager bool
HasManager bool
Grade string
JobTitle string
PositionID string
Source string
@@ -267,6 +331,7 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
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...)
@@ -277,6 +342,7 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
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...)
@@ -286,6 +352,10 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
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
@@ -303,8 +373,8 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
continue
}
if isWorksmobileDomainRootTenant(appointmentTenant) {
if appointment.IsPrimary && strings.TrimSpace(appointment.JobTitle) != "" && task == "" {
task = strings.TrimSpace(appointment.JobTitle)
if appointment.IsPrimary && strings.TrimSpace(appointment.JobTitle) != "" && primaryTask == "" {
primaryTask = strings.TrimSpace(appointment.JobTitle)
}
seen[appointment.TenantID] = true
continue
@@ -317,50 +387,104 @@ func buildWorksmobileUserOrganizations(user domain.User, tenant domain.Tenant, t
if err != nil {
return nil, "", err
}
isPrimaryOrganization := !worksmobileOrganizationsHavePrimary(organizations)
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 isPrimaryOrganization {
organizations[organizationIndex].Primary = true
organizations[organizationIndex].Email = worksmobileOrganizationEmail(user, domainTenant)
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),
Primary: isPrimaryOrganization,
OrgUnits: []WorksmobileUserOrgUnit{orgUnit},
DomainID: domainID,
Email: worksmobileOrganizationEmail(user, domainTenant),
LevelID: levelID,
LevelName: levelName,
OrgUnits: []WorksmobileUserOrgUnit{orgUnit},
})
}
if isPrimaryOrganization && strings.TrimSpace(appointment.JobTitle) != "" {
task = strings.TrimSpace(appointment.JobTitle)
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
}
if !worksmobileOrganizationsHavePrimary(organizations) {
organizations[0].Primary = true
if len(organizations[0].OrgUnits) > 0 {
organizations[0].OrgUnits[0].Primary = true
}
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 == "" {
@@ -466,6 +590,15 @@ func worksmobileOrganizationsHavePrimary(organizations []WorksmobileUserOrganiza
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 {
@@ -480,6 +613,7 @@ func worksmobileAppointmentsFromMetadata(metadata domain.JSONMap) []worksmobileA
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"),
@@ -493,6 +627,193 @@ func worksmobileAppointmentsFromMetadata(metadata domain.JSONMap) []worksmobileA
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 {