1
0
forked from baron/baron-sso

merge: integrate origin dev into dev

Includes Worksmobile SSOT sync comparison updates, UUID import conflict resolution, and Playwright route mock stabilization.
This commit is contained in:
2026-06-01 17:48:39 +09:00
91 changed files with 2173 additions and 1268 deletions

View File

@@ -5,6 +5,7 @@ import (
"baron-sso-backend/internal/repository"
"context"
"errors"
"fmt"
"net/mail"
"os"
"sort"
@@ -1266,7 +1267,7 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
if !matched {
remote, matched = remoteByEmail[strings.ToLower(strings.TrimSpace(user.Email))]
}
needsUpdate := matched && worksmobileUserNeedsUpdate(user, remote)
needsUpdate := matched && worksmobileUserNeedsUpdate(user, remote, localTenants)
if matched && !includeMatched && !needsUpdate {
matchedRemoteIDs[remote.ID] = true
continue
@@ -1364,7 +1365,7 @@ func compareWorksmobileUsers(localUsers []domain.User, remoteUsers []Worksmobile
return result
}
func worksmobileUserNeedsUpdate(user domain.User, remote WorksmobileRemoteUser) bool {
func worksmobileUserNeedsUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant) bool {
if strings.TrimSpace(remote.ExternalID) != strings.TrimSpace(user.ID) {
return true
}
@@ -1374,12 +1375,179 @@ func worksmobileUserNeedsUpdate(user domain.User, remote WorksmobileRemoteUser)
if strings.ToLower(strings.TrimSpace(remote.Email)) != strings.ToLower(strings.TrimSpace(user.Email)) {
return true
}
if worksmobileUserPhoneNeedsUpdate(user, remote) {
return true
}
if worksmobileUserEmployeeNumberNeedsUpdate(user, remote) {
return true
}
if worksmobileUserOrganizationsNeedUpdate(user, remote, localTenants) {
return true
}
if worksmobileUserManagerNeedsUpdate(user, remote) {
return true
}
return false
}
func worksmobileUserPhoneNeedsUpdate(user domain.User, remote WorksmobileRemoteUser) bool {
localPhone := normalizeWorksmobilePhoneForCompare(user.Phone)
remotePhone := normalizeWorksmobilePhoneForCompare(remote.CellPhone)
if localPhone == "" && remotePhone == "" {
return false
}
return localPhone != remotePhone
}
func normalizeWorksmobilePhoneForCompare(value string) string {
normalized := strings.TrimSpace(value)
normalized = strings.NewReplacer("-", "", " ", "", "(", "", ")", "").Replace(normalized)
if normalized == "" {
return ""
}
if strings.HasPrefix(normalized, "010") {
return "+82" + normalized[1:]
}
if strings.HasPrefix(normalized, "82") {
return "+" + normalized
}
return normalized
}
func worksmobileUserEmployeeNumberNeedsUpdate(user domain.User, remote WorksmobileRemoteUser) bool {
localEmployeeNumber := strings.TrimSpace(metadataEmployeeNumber(user.Metadata))
remoteEmployeeNumber := strings.TrimSpace(remote.EmployeeNumber)
if localEmployeeNumber == "" && remoteEmployeeNumber == "" {
return false
}
return localEmployeeNumber != remoteEmployeeNumber
}
func worksmobileUserOrganizationsNeedUpdate(user domain.User, remote WorksmobileRemoteUser, localTenants map[string]domain.Tenant) bool {
if len(remote.Organizations) == 0 || user.TenantID == nil || localTenants == nil {
return false
}
tenantID := strings.TrimSpace(*user.TenantID)
if tenantID == "" {
return false
}
tenant, ok := localTenants[tenantID]
if !ok {
return false
}
expected, err := BuildWorksmobileUserPayloadForDomainTenants(user, tenant, localTenants, worksmobileComparisonRootConfig(localTenants))
if err != nil {
return false
}
return !worksmobileUserOrganizationsEqual(expected.Organizations, remote.Organizations)
}
func worksmobileComparisonRootConfig(localTenants map[string]domain.Tenant) domain.JSONMap {
for _, tenant := range localTenants {
if strings.TrimSpace(tenant.Slug) == HanmacFamilyTenantSlug {
return tenant.Config
}
}
return nil
}
type worksmobileComparableOrgUnit struct {
organizationPrimary bool
organizationEmail string
unitPrimary bool
positionID string
comparePosition bool
manager *bool
compareManager bool
}
func worksmobileUserOrganizationsEqual(expected []WorksmobileUserOrganization, remote []WorksmobileUserOrganization) bool {
expectedUnits := flattenExpectedWorksmobileUserOrganizations(expected)
remoteUnits := flattenRemoteWorksmobileUserOrganizations(remote)
if len(expectedUnits) != len(remoteUnits) {
return false
}
for key, expectedUnit := range expectedUnits {
remoteUnit, ok := remoteUnits[key]
if !ok {
return false
}
if expectedUnit.organizationPrimary != remoteUnit.organizationPrimary {
return false
}
if strings.ToLower(expectedUnit.organizationEmail) != strings.ToLower(remoteUnit.organizationEmail) {
return false
}
if expectedUnit.unitPrimary != remoteUnit.unitPrimary {
return false
}
if expectedUnit.comparePosition && strings.TrimSpace(expectedUnit.positionID) != strings.TrimSpace(remoteUnit.positionID) {
return false
}
if expectedUnit.compareManager && !worksmobileBoolPointersEqual(expectedUnit.manager, remoteUnit.manager) {
return false
}
}
return true
}
func flattenExpectedWorksmobileUserOrganizations(organizations []WorksmobileUserOrganization) map[string]worksmobileComparableOrgUnit {
result := map[string]worksmobileComparableOrgUnit{}
for _, organization := range organizations {
for _, orgUnit := range organization.OrgUnits {
key := worksmobileComparableOrgUnitKey(organization.DomainID, orgUnit.OrgUnitID)
if key == "" {
continue
}
result[key] = worksmobileComparableOrgUnit{
organizationPrimary: organization.Primary,
organizationEmail: strings.TrimSpace(organization.Email),
unitPrimary: orgUnit.Primary,
positionID: strings.TrimSpace(orgUnit.PositionID),
comparePosition: strings.TrimSpace(orgUnit.PositionID) != "",
manager: orgUnit.IsManager,
compareManager: orgUnit.IsManager != nil,
}
}
}
return result
}
func flattenRemoteWorksmobileUserOrganizations(organizations []WorksmobileUserOrganization) map[string]worksmobileComparableOrgUnit {
result := map[string]worksmobileComparableOrgUnit{}
for _, organization := range organizations {
for _, orgUnit := range organization.OrgUnits {
key := worksmobileComparableOrgUnitKey(organization.DomainID, orgUnit.OrgUnitID)
if key == "" {
continue
}
result[key] = worksmobileComparableOrgUnit{
organizationPrimary: organization.Primary,
organizationEmail: strings.TrimSpace(organization.Email),
unitPrimary: orgUnit.Primary,
positionID: strings.TrimSpace(orgUnit.PositionID),
manager: orgUnit.IsManager,
}
}
}
return result
}
func worksmobileComparableOrgUnitKey(domainID int64, orgUnitID string) string {
orgUnitID = strings.TrimSpace(orgUnitID)
if domainID == 0 || orgUnitID == "" {
return ""
}
return fmt.Sprintf("%d:%s", domainID, orgUnitID)
}
func worksmobileBoolPointersEqual(left, right *bool) bool {
if left == nil || right == nil {
return left == nil && right == nil
}
return *left == *right
}
func worksmobileUserManagerNeedsUpdate(user domain.User, remote WorksmobileRemoteUser) bool {
localManagers := worksmobileUserExplicitOrgUnitManagers(user)
if len(localManagers) == 0 {