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

@@ -43,6 +43,9 @@ type worksmobileSyncConfig struct {
CreateUsersResultOutput string
CreateUsersLimit int
CreateUsersForcePasswordChange bool
UpdateUserLevelsCSV string
UpdateUserLevelsResultOutput string
UpdateUserLevelsLimit int
ImportHanmacUsersCSV string
ImportHanmacUsersResultOutput string
ImportHanmacUsersPassword string
@@ -168,6 +171,11 @@ func runWorksmobileSync(args []string) error {
return err
}
}
if config.UpdateUserLevelsCSV != "" {
if err := updateWorksmobileUserLevelsFromCSV(ctx, db, tenantRepo, userRepo, *root, config.UpdateUserLevelsCSV, config.UpdateUserLevelsResultOutput, config.UpdateUserLevelsLimit, newWorksmobileAdminClient()); err != nil {
return err
}
}
if config.ImportHanmacUsersCSV != "" {
if err := importHanmacUsersAndCreateWorksmobileAccounts(ctx, db, tenantRepo, userRepo, *root, config.ImportHanmacUsersCSV, config.ImportHanmacUsersResultOutput, config.ImportHanmacUsersPassword, config.ImportHanmacUsersLimit, config.ImportHanmacUsersForcePasswordChange, newWorksmobileAdminClient()); err != nil {
return err
@@ -232,6 +240,9 @@ func resolveWorksmobileSyncConfig(args []string) (worksmobileSyncConfig, error)
fs.StringVar(&config.CreateUsersResultOutput, "create-users-result-output", "", "output CSV path for Worksmobile create results")
fs.IntVar(&config.CreateUsersLimit, "create-users-limit", 0, "maximum users to create; 0 means all")
fs.BoolVar(&config.CreateUsersForcePasswordChange, "create-users-force-password-change", true, "request password change at next login for --create-users-csv")
fs.StringVar(&config.UpdateUserLevelsCSV, "update-user-levels-csv", "", "CSV containing user_id column to patch Worksmobile user levels directly")
fs.StringVar(&config.UpdateUserLevelsResultOutput, "update-user-levels-result-output", "", "output CSV path for Worksmobile user level patch results")
fs.IntVar(&config.UpdateUserLevelsLimit, "update-user-levels-limit", 0, "maximum users to patch levels; 0 means all")
fs.StringVar(&config.ImportHanmacUsersCSV, "import-hanmac-users-csv", "", "CSV containing Hanmac internal users to upsert into Baron and create in Worksmobile")
fs.StringVar(&config.ImportHanmacUsersResultOutput, "import-hanmac-users-result-output", "", "output CSV path for Hanmac user import and Worksmobile create results")
fs.StringVar(&config.ImportHanmacUsersPassword, "import-hanmac-users-password", "", "initial password for --import-hanmac-users-csv")
@@ -256,8 +267,8 @@ func resolveWorksmobileSyncConfig(args []string) (worksmobileSyncConfig, error)
if err := fs.Parse(args); err != nil {
return config, err
}
if !config.SyncOrgUnits && config.UsersCSV == "" && config.InspectUsersCSV == "" && config.InspectOrgUnitsCSV == "" && config.UpsertOrgUnitID == "" && config.UndeleteUsersCSV == "" && config.RemoveAliasesCSV == "" && config.FindNumberStrippedAliasesOutput == "" && config.DuplicatePhoneCountryCodeOutput == "" && !config.FixDuplicatePhoneCountryCode && config.PendingUsersOutput == "" && config.ResetPendingUsersPassword == "" && config.DeletePendingUsersResultOutput == "" && config.ForceDeleteUsersCSV == "" && config.CreateUsersCSV == "" && config.ImportHanmacUsersCSV == "" && config.RecreatePendingUsersPassword == "" && config.ActivateAllUsersOutput == "" && config.ComparisonOutput == "" && config.AlignBaronFromWorksOutput == "" && !config.Process {
return config, fmt.Errorf("nothing to do; pass --orgunits, --users-csv, --inspect-users-csv, --inspect-orgunits-csv, --upsert-orgunit-id, --undelete-users-csv, --remove-aliases-csv, --find-number-stripped-aliases-output, --duplicate-phone-country-code-output, --fix-duplicate-phone-country-code, --pending-users-output, --reset-pending-users-password, --delete-pending-users-result-output, --force-delete-users-csv, --create-users-csv, --import-hanmac-users-csv, --recreate-pending-users-password, --activate-all-users-output, --comparison-output, --align-baron-from-works-output, or --process")
if !config.SyncOrgUnits && config.UsersCSV == "" && config.InspectUsersCSV == "" && config.InspectOrgUnitsCSV == "" && config.UpsertOrgUnitID == "" && config.UndeleteUsersCSV == "" && config.RemoveAliasesCSV == "" && config.FindNumberStrippedAliasesOutput == "" && config.DuplicatePhoneCountryCodeOutput == "" && !config.FixDuplicatePhoneCountryCode && config.PendingUsersOutput == "" && config.ResetPendingUsersPassword == "" && config.DeletePendingUsersResultOutput == "" && config.ForceDeleteUsersCSV == "" && config.CreateUsersCSV == "" && config.UpdateUserLevelsCSV == "" && config.ImportHanmacUsersCSV == "" && config.RecreatePendingUsersPassword == "" && config.ActivateAllUsersOutput == "" && config.ComparisonOutput == "" && config.AlignBaronFromWorksOutput == "" && !config.Process {
return config, fmt.Errorf("nothing to do; pass --orgunits, --users-csv, --inspect-users-csv, --inspect-orgunits-csv, --upsert-orgunit-id, --undelete-users-csv, --remove-aliases-csv, --find-number-stripped-aliases-output, --duplicate-phone-country-code-output, --fix-duplicate-phone-country-code, --pending-users-output, --reset-pending-users-password, --delete-pending-users-result-output, --force-delete-users-csv, --create-users-csv, --update-user-levels-csv, --import-hanmac-users-csv, --recreate-pending-users-password, --activate-all-users-output, --comparison-output, --align-baron-from-works-output, or --process")
}
if config.ResetPendingUsersPassword != "" && config.ResetPendingUsersResultOutput == "" {
return config, fmt.Errorf("--reset-pending-users-result-output is required with --reset-pending-users-password")
@@ -277,6 +288,9 @@ func resolveWorksmobileSyncConfig(args []string) (worksmobileSyncConfig, error)
if config.CreateUsersCSV != "" && config.CreateUsersResultOutput == "" {
return config, fmt.Errorf("--create-users-result-output is required with --create-users-csv")
}
if config.UpdateUserLevelsCSV != "" && config.UpdateUserLevelsResultOutput == "" {
return config, fmt.Errorf("--update-user-levels-result-output is required with --update-user-levels-csv")
}
if config.ImportHanmacUsersCSV != "" && config.ImportHanmacUsersPassword == "" {
return config, fmt.Errorf("--import-hanmac-users-password is required with --import-hanmac-users-csv")
}
@@ -1346,6 +1360,134 @@ func createWorksmobileUsersFromCSV(ctx context.Context, db *gorm.DB, tenantRepo
return nil
}
func updateWorksmobileUserLevelsFromCSV(ctx context.Context, db *gorm.DB, tenantRepo repository.TenantRepository, userRepo repository.UserRepository, root domain.Tenant, usersCSV string, outputPath string, limit int, client *service.WorksmobileHTTPClient) error {
if limit < 0 {
return fmt.Errorf("--update-user-levels-limit cannot be negative")
}
userIDs, err := readWorksmobileUserIDsCSV(usersCSV)
if err != nil {
return err
}
if limit > 0 && len(userIDs) > limit {
userIDs = userIDs[:limit]
}
tenantIDs, err := activeTenantSubtreeIDs(ctx, db, root.ID)
if err != nil {
return err
}
tenants, err := tenantRepo.FindByIDs(ctx, tenantIDs)
if err != nil {
return err
}
tenantByID := map[string]domain.Tenant{root.ID: root}
for _, tenant := range tenants {
tenantByID[tenant.ID] = tenant
}
users, err := userRepo.FindByIDs(ctx, userIDs)
if err != nil {
return err
}
userByID := map[string]domain.User{}
for _, user := range users {
userByID[user.ID] = user
}
file, err := os.Create(outputPath)
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
header := []string{"user_id", "email", "name", "domain_id", "baron_level", "status", "error"}
if err := writer.Write(header); err != nil {
return err
}
okCount := 0
skippedCount := 0
errorCount := 0
for _, userID := range userIDs {
status := "ok"
errorMessage := ""
email := ""
name := ""
domainID := ""
levelName := ""
user, ok := userByID[userID]
if !ok {
status = "skipped"
errorMessage = "baron user not found"
skippedCount++
} else {
email = strings.TrimSpace(user.Email)
name = strings.TrimSpace(user.Name)
if user.TenantID == nil {
status = "skipped"
errorMessage = "baron user has no tenant"
skippedCount++
} else if !domain.IsWorksProvisionedUserStatus(user.Status) {
status = "skipped"
errorMessage = "baron user status is excluded from Worksmobile sync"
skippedCount++
} else {
tenant, ok := tenantByID[*user.TenantID]
if !ok {
status = "skipped"
errorMessage = "baron user tenant is outside Worksmobile sync scope"
skippedCount++
} else {
payload, err := service.BuildWorksmobileUserPayloadForDomainTenants(user, tenant, tenantByID, root.Config)
if err != nil {
status = "skipped"
errorMessage = err.Error()
skippedCount++
} else {
levelDomainID := worksmobileUserLevelPatchDomainID(payload)
domainID = fmt.Sprint(levelDomainID)
levelName = strings.TrimSpace(payload.LevelID)
expectedLevelName := service.WorksmobileLevelDisplayNameForIdentifier(levelName)
if levelName == "" {
status = "skipped"
errorMessage = "baron user has no level"
skippedCount++
} else if err := client.PatchUserOrganizationLevelByName(ctx, payload.Email, levelDomainID, levelName); err != nil {
status = "error"
errorMessage = err.Error()
errorCount++
} else if remote, err := client.GetUser(ctx, payload.Email); err != nil {
status = "error"
errorMessage = err.Error()
errorCount++
} else if !service.WorksmobileLevelIdentifierMatchesRemote(levelName, remote.LevelID, remote.LevelName) {
status = "error"
errorMessage = fmt.Sprintf("worksmobile level verification failed: expected_level=%s expected_level_name=%s remote_level_id=%s remote_level_name=%s", levelName, expectedLevelName, strings.TrimSpace(remote.LevelID), strings.TrimSpace(remote.LevelName))
errorCount++
} else {
okCount++
}
}
}
}
}
if err := writer.Write([]string{userID, email, name, domainID, levelName, status, errorMessage}); err != nil {
return err
}
writer.Flush()
if err := writer.Error(); err != nil {
return err
}
}
fmt.Printf("worksmobile user levels update result written: %s targets=%d ok=%d skipped=%d errors=%d\n", outputPath, len(userIDs), okCount, skippedCount, errorCount)
return nil
}
func worksmobileUserLevelPatchDomainID(payload service.WorksmobileUserPayload) int64 {
if payload.LevelDomainID > 0 {
return payload.LevelDomainID
}
return payload.DomainID
}
type hanmacWorksmobileImportRow struct {
Email string
Name string
@@ -1698,6 +1840,7 @@ func applyHanmacWorksmobileImportRowToUser(user *domain.User, row hanmacWorksmob
"tenantId": tenant.ID,
"tenantSlug": tenant.Slug,
"tenantName": tenant.Name,
"grade": strings.TrimSpace(row.Grade),
"isPrimary": true,
}}
}
@@ -2300,6 +2443,7 @@ func exportWorksmobileNeedsUpdateComparison(ctx context.Context, syncService ser
"baron_id",
"baron_name",
"baron_email",
"baron_grade",
"baron_primary_org_id",
"baron_primary_org_slug",
"baron_primary_org_name",
@@ -2336,6 +2480,7 @@ func exportWorksmobileNeedsUpdateComparison(ctx context.Context, syncService ser
item.BaronID,
item.BaronName,
item.BaronEmail,
item.BaronGrade,
item.BaronPrimaryOrgID,
item.BaronPrimaryOrgSlug,
item.BaronPrimaryOrgName,
@@ -3061,7 +3206,7 @@ func newWorksmobileAdminClient() *service.WorksmobileHTTPClient {
},
)
client.BaseURL = strings.TrimSpace(getenv("WORKS_ADMIN_API_BASE_URL", ""))
client.RateLimiter = service.NewWorksmobileAPIRateLimiter(180, time.Minute)
client.RateLimiter = service.NewWorksmobileAPIRateLimiter(240, time.Minute)
return client
}
@@ -3071,7 +3216,7 @@ func newWorksmobileSCIMClient() *service.WorksmobileHTTPClient {
getenv("WORKS_ADMIN_SCIM_TOKEN", getenv("SAMAN_SCIM_LONGLIVE_TOKEN", "")),
)
client.BaseURL = strings.TrimSpace(getenv("WORKS_ADMIN_API_BASE_URL", ""))
client.RateLimiter = service.NewWorksmobileAPIRateLimiter(180, time.Minute)
client.RateLimiter = service.NewWorksmobileAPIRateLimiter(240, time.Minute)
return client
}