1
0
forked from baron/baron-sso

feat: implement multi-tenant member management and UI improvements

- Add multi-tenant support (isAddTenant, isRemoveTenant) to backend UpdateUser API.
- Update UserRepository to support searching in company_codes array.
- Implement table sorting and align search bar layout in adminfront.
- Add 'Assign Existing Member' and 'Exclude from Organization' features to TenantUsersPage.
- Auto-populate tenantSlug in UserCreatePage via query parameters.
- Add necessary localization keys for new UI elements.

Resolves #644, #639, #642, #641
This commit is contained in:
2026-05-06 14:20:35 +09:00
parent 3169dd958a
commit 5f9a61de98
10 changed files with 591 additions and 140 deletions

View File

@@ -1432,6 +1432,8 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
Role *string `json:"role"`
Status *string `json:"status"`
CompanyCode *string `json:"companyCode"`
IsAddTenant bool `json:"isAddTenant"`
IsRemoveTenant bool `json:"isRemoveTenant"`
Department *string `json:"department"`
Position *string `json:"position"`
JobTitle *string `json:"jobTitle"`
@@ -1446,9 +1448,9 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
req.Metadata = mergeUserAppointmentMetadata(req.Metadata, req.AdditionalAppointments, req.PrimaryTenantID, req.PrimaryTenantName, req.PrimaryTenantIsOwner)
// [New] Tenant Admin restriction: Cannot change companyCode
// [New] Tenant Admin restriction: Cannot change companyCode (except when adding/removing secondary membership)
if requester != nil && domain.NormalizeRole(requester.Role) == domain.RoleTenantAdmin {
if req.CompanyCode != nil && *req.CompanyCode != requester.CompanyCode {
if !req.IsAddTenant && !req.IsRemoveTenant && req.CompanyCode != nil && *req.CompanyCode != requester.CompanyCode {
return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant")
}
}
@@ -1531,38 +1533,80 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
}
if req.CompanyCode != nil {
code := strings.TrimSpace(*req.CompanyCode)
traits["companyCode"] = code
// Resolve TenantID for Kratos Trait
if h.TenantService != nil && code != "" {
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil {
traits["tenant_id"] = tenant.ID
}
}
// Add to existingCodes if not present
found := false
for _, existing := range existingCodes {
if existing == code {
found = true
break
if req.IsAddTenant {
// Add to existingCodes if not present
found := false
for _, existing := range existingCodes {
if existing == code {
found = true
break
}
}
if !found && code != "" {
existingCodes = append(existingCodes, code)
}
} else if req.IsRemoveTenant {
// Remove from existingCodes
var newCodes []string
for _, existing := range existingCodes {
if existing != code {
newCodes = append(newCodes, existing)
}
}
existingCodes = newCodes
// If removing the primary company code, pick another one as primary if available
currentPrimary := extractTraitString(traits, "companyCode")
if currentPrimary == code {
if len(existingCodes) > 0 {
traits["companyCode"] = existingCodes[0]
if h.TenantService != nil {
if t, err := h.TenantService.GetTenantBySlug(c.Context(), existingCodes[0]); err == nil && t != nil {
traits["tenant_id"] = t.ID
}
}
} else {
traits["companyCode"] = ""
traits["tenant_id"] = ""
}
}
} else {
// Normal update: replace primary company code and ensure it's in existingCodes
traits["companyCode"] = code
// Resolve TenantID for Kratos Trait
if h.TenantService != nil && code != "" {
if tenant, err := h.TenantService.GetTenantBySlug(c.Context(), code); err == nil && tenant != nil {
traits["tenant_id"] = tenant.ID
}
}
found := false
for _, existing := range existingCodes {
if existing == code {
found = true
break
}
}
if !found && code != "" {
existingCodes = append(existingCodes, code)
}
}
if !found && code != "" {
existingCodes = append(existingCodes, code)
}
}
// Deduplicate and save back companyCodes
var uniqueCodes []string
var codesToSave []string
seenCodes := map[string]bool{}
for _, c := range existingCodes {
if !seenCodes[c] && c != "" {
seenCodes[c] = true
uniqueCodes = append(uniqueCodes, c)
codesToSave = append(codesToSave, c)
}
}
if len(uniqueCodes) > 0 {
traits["companyCodes"] = uniqueCodes
if len(codesToSave) > 0 {
traits["companyCodes"] = codesToSave
} else {
delete(traits, "companyCodes")
}
if req.Department != nil {
@@ -1927,6 +1971,17 @@ func (h *UserHandler) mapToLocalUser(identity service.KratosIdentity) *domain.Us
UpdatedAt: identity.UpdatedAt,
}
// [New] Sync multi-tenant codes
if codes, ok := traits["companyCodes"].([]interface{}); ok {
for _, v := range codes {
if str, ok := v.(string); ok && str != "" {
user.CompanyCodes = append(user.CompanyCodes, str)
}
}
} else if codes, ok := traits["companyCodes"].([]string); ok {
user.CompanyCodes = codes
}
// 1. Try to get tenant_id directly from Kratos traits first (Fastest & most reliable)
tID := extractTraitString(traits, "tenant_id")
if tID != "" {