forked from baron/baron-sso
feat: update worksmobile sync and restore planning
This commit is contained in:
@@ -13,8 +13,10 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -850,6 +852,7 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
type bulkUserItem struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
LoginID string `json:"loginId"`
|
||||
Name string `json:"name"`
|
||||
@@ -906,6 +909,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
var hanmacScope *hanmacEmailScope
|
||||
var hanmacLocalParts map[string]bool
|
||||
hanmacScopeLoaded := false
|
||||
bulkEmailErrors := validateBulkUserEmailUniqueness(req.Users)
|
||||
|
||||
// Pre-fetch tenant data to avoid redundant DB calls
|
||||
type tenantCacheItem struct {
|
||||
@@ -1011,7 +1015,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
return cacheTenantItem(buildTenantCacheItem(tenant)), nil
|
||||
}
|
||||
|
||||
for _, item := range req.Users {
|
||||
for index, item := range req.Users {
|
||||
email := strings.TrimSpace(item.Email)
|
||||
name := strings.TrimSpace(item.Name)
|
||||
tenantID := strings.TrimSpace(item.TenantID)
|
||||
@@ -1026,6 +1030,10 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Message: tenantSlugErr.Error()})
|
||||
continue
|
||||
}
|
||||
if message, exists := bulkEmailErrors[index]; exists {
|
||||
results = append(results, bulkUserResult{Email: email, Success: false, Status: "blockingError", Message: message})
|
||||
continue
|
||||
}
|
||||
|
||||
var tItem tenantCacheItem
|
||||
var err error
|
||||
@@ -1192,6 +1200,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
item.Metadata["additionalAppointments"] = resolvedAppointments
|
||||
}
|
||||
normalizeBulkUserAliasMetadata(item.Metadata)
|
||||
item.Metadata = sanitizeUserMetadata(item.Metadata)
|
||||
|
||||
password, _ := utils.GeneratePasswordWithPolicy(policy)
|
||||
@@ -1252,6 +1261,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
identityID, err := h.OryProvider.CreateUser(&domain.BrokerUser{
|
||||
ID: strings.TrimSpace(item.UserID),
|
||||
Email: userEmail,
|
||||
Name: item.Name,
|
||||
PhoneNumber: userPhone,
|
||||
@@ -1845,6 +1855,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email *string `json:"email"`
|
||||
LoginID *string `json:"loginId"`
|
||||
Password *string `json:"password"`
|
||||
Name *string `json:"name"`
|
||||
@@ -1948,6 +1959,31 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
if traits == nil {
|
||||
traits = map[string]interface{}{}
|
||||
}
|
||||
if req.Email != nil {
|
||||
currentEmail := strings.TrimSpace(extractTraitString(traits, "email"))
|
||||
nextEmail := strings.ToLower(strings.TrimSpace(*req.Email))
|
||||
if nextEmail == "" {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "email is required")
|
||||
}
|
||||
parsed, parseErr := mail.ParseAddress(nextEmail)
|
||||
if parseErr != nil || !strings.EqualFold(parsed.Address, nextEmail) {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid email")
|
||||
}
|
||||
if !strings.EqualFold(currentEmail, nextEmail) {
|
||||
if requester == nil || domain.NormalizeRole(requester.Role) != domain.RoleSuperAdmin {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: only super admin can change user email")
|
||||
}
|
||||
if h.UserRepo != nil {
|
||||
if existing, err := h.UserRepo.FindByEmail(c.Context(), nextEmail); err == nil && existing != nil && existing.ID != userID {
|
||||
return errorJSON(c, fiber.StatusConflict, "email is already used by another user")
|
||||
}
|
||||
if taken, err := h.UserRepo.IsLoginIDTaken(c.Context(), nextEmail); err == nil && taken {
|
||||
return errorJSON(c, fiber.StatusConflict, "email is already used as a login ID")
|
||||
}
|
||||
}
|
||||
traits["email"] = nextEmail
|
||||
}
|
||||
}
|
||||
delete(traits, "hanmacFamily")
|
||||
delete(traits, "userType")
|
||||
|
||||
@@ -2048,6 +2084,13 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if subEmailRaw, exists := req.Metadata["sub_email"]; exists {
|
||||
subEmails := normalizeUserSubEmailValues(subEmailRaw)
|
||||
traits["sub_email"] = subEmails
|
||||
traits["aliasEmails"] = subEmails
|
||||
traits["secondary_emails"] = subEmails
|
||||
traits["worksmobileAliasEmails"] = subEmails
|
||||
}
|
||||
|
||||
// [LoginID Sync based on Tenant Settings]
|
||||
// Perform sync AFTER metadata merge to ensure traits contains current values
|
||||
@@ -2860,6 +2903,156 @@ func normalizeCustomLoginIDsTrait(traits map[string]interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeUserSubEmailValues(raw any) []interface{} {
|
||||
values := make([]string, 0)
|
||||
switch typed := raw.(type) {
|
||||
case []string:
|
||||
values = append(values, typed...)
|
||||
case []interface{}:
|
||||
for _, item := range typed {
|
||||
values = append(values, fmt.Sprint(item))
|
||||
}
|
||||
case string:
|
||||
values = append(values, typed)
|
||||
default:
|
||||
if raw != nil {
|
||||
values = append(values, fmt.Sprint(raw))
|
||||
}
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
result := make([]interface{}, 0, len(values))
|
||||
for _, value := range values {
|
||||
for _, part := range strings.Split(value, ",") {
|
||||
normalized := strings.ToLower(strings.TrimSpace(part))
|
||||
if normalized == "" || seen[normalized] {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = true
|
||||
result = append(result, normalized)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func validateBulkUserEmailUniqueness(users []bulkUserItem) map[int]string {
|
||||
owners := map[string]map[int]bool{}
|
||||
errorsByIndex := map[int]string{}
|
||||
|
||||
for index, user := range users {
|
||||
primaryEmail := normalizeBulkUserEmail(user.Email)
|
||||
aliases := bulkUserAliasEmailSet(user.Metadata)
|
||||
rowEmails := map[string]bool{}
|
||||
if primaryEmail != "" {
|
||||
rowEmails[primaryEmail] = true
|
||||
}
|
||||
for alias := range aliases {
|
||||
if primaryEmail != "" && alias == primaryEmail {
|
||||
errorsByIndex[index] = "duplicate email in bulk request: " + alias
|
||||
}
|
||||
rowEmails[alias] = true
|
||||
}
|
||||
for email := range rowEmails {
|
||||
if owners[email] == nil {
|
||||
owners[email] = map[int]bool{}
|
||||
}
|
||||
owners[email][index] = true
|
||||
}
|
||||
}
|
||||
|
||||
for email, indexes := range owners {
|
||||
if len(indexes) < 2 {
|
||||
continue
|
||||
}
|
||||
for index := range indexes {
|
||||
errorsByIndex[index] = "duplicate email in bulk request: " + email
|
||||
}
|
||||
}
|
||||
|
||||
return errorsByIndex
|
||||
}
|
||||
|
||||
func normalizeBulkUserAliasMetadata(metadata map[string]any) {
|
||||
if metadata == nil {
|
||||
return
|
||||
}
|
||||
aliases := bulkUserAliasEmailSet(metadata)
|
||||
hasAliasField := false
|
||||
for _, key := range []string{"sub_email", "aliasEmails", "secondary_emails", "worksmobileAliasEmails"} {
|
||||
if _, exists := metadata[key]; exists {
|
||||
hasAliasField = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAliasField {
|
||||
return
|
||||
}
|
||||
values := make([]interface{}, 0, len(aliases))
|
||||
for alias := range aliases {
|
||||
values = append(values, alias)
|
||||
}
|
||||
sort.Slice(values, func(i, j int) bool {
|
||||
return fmt.Sprint(values[i]) < fmt.Sprint(values[j])
|
||||
})
|
||||
metadata["sub_email"] = values
|
||||
metadata["aliasEmails"] = values
|
||||
metadata["secondary_emails"] = values
|
||||
metadata["worksmobileAliasEmails"] = values
|
||||
}
|
||||
|
||||
func bulkUserAliasEmailSet(metadata map[string]any) map[string]bool {
|
||||
aliases := map[string]bool{}
|
||||
for _, key := range []string{"sub_email", "aliasEmails", "secondary_emails", "worksmobileAliasEmails"} {
|
||||
for _, value := range bulkUserEmailValues(metadata[key]) {
|
||||
aliases[value] = true
|
||||
}
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
func bulkUserEmailValues(raw any) []string {
|
||||
values := make([]string, 0)
|
||||
switch typed := raw.(type) {
|
||||
case []string:
|
||||
values = append(values, typed...)
|
||||
case []interface{}:
|
||||
for _, item := range typed {
|
||||
values = append(values, fmt.Sprint(item))
|
||||
}
|
||||
case string:
|
||||
values = append(values, typed)
|
||||
default:
|
||||
if raw != nil {
|
||||
values = append(values, fmt.Sprint(raw))
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
for _, token := range strings.FieldsFunc(value, func(r rune) bool {
|
||||
return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t'
|
||||
}) {
|
||||
email := normalizeBulkUserEmail(token)
|
||||
if email != "" {
|
||||
result = append(result, email)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeBulkUserEmail(value string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
if normalized == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := mail.ParseAddress(normalized)
|
||||
if err != nil {
|
||||
return normalized
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(parsed.Address))
|
||||
}
|
||||
|
||||
func formatTime(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user