forked from baron/baron-sso
chore: snapshot local state before dev merge
This commit is contained in:
@@ -95,11 +95,41 @@ func sanitizeUserMetadata(metadata map[string]any) map[string]any {
|
||||
if key == "hanmacFamily" || key == "userType" {
|
||||
continue
|
||||
}
|
||||
if key == "additionalAppointments" {
|
||||
sanitized[key] = normalizeUserAppointmentGrades(value)
|
||||
continue
|
||||
}
|
||||
sanitized[key] = value
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func normalizeUserAppointmentGrades(raw any) []any {
|
||||
appointments := userAppointmentSliceFromRaw(raw)
|
||||
for i, item := range appointments {
|
||||
appointment, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if grade, ok := appointment["grade"].(string); ok {
|
||||
appointment["grade"] = normalizeInternalGradeName(grade)
|
||||
}
|
||||
appointments[i] = appointment
|
||||
}
|
||||
return appointments
|
||||
}
|
||||
|
||||
func normalizeInternalGradeName(grade string) string {
|
||||
switch strings.ReplaceAll(strings.TrimSpace(grade), " ", "") {
|
||||
case "상무":
|
||||
return "상무이사"
|
||||
case "전무":
|
||||
return "전무이사"
|
||||
default:
|
||||
return strings.TrimSpace(grade)
|
||||
}
|
||||
}
|
||||
|
||||
func userAppointmentSliceFromRaw(raw any) []any {
|
||||
switch values := raw.(type) {
|
||||
case []any:
|
||||
@@ -142,6 +172,144 @@ func userAppointmentTenantKey(raw any) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func userAppointmentMatchesTenant(appointment map[string]any, tenantID string, tenantSlug string) bool {
|
||||
targetID := strings.ToLower(strings.TrimSpace(tenantID))
|
||||
targetSlug := strings.ToLower(strings.TrimSpace(tenantSlug))
|
||||
appointmentID := strings.ToLower(normalizeMetadataString(appointment["tenantId"]))
|
||||
appointmentSlug := strings.ToLower(normalizeMetadataString(appointment["tenantSlug"]))
|
||||
if appointmentSlug == "" {
|
||||
appointmentSlug = strings.ToLower(normalizeMetadataString(appointment["slug"]))
|
||||
}
|
||||
return (targetID != "" && appointmentID == targetID) ||
|
||||
(targetSlug != "" && appointmentSlug == targetSlug)
|
||||
}
|
||||
|
||||
func tenantBoundGradeFromTraits(traits map[string]any) string {
|
||||
tenantID := extractTraitString(traits, "tenant_id")
|
||||
tenantSlug := extractTraitString(traits, "tenantSlug")
|
||||
appointments := userAppointmentSliceFromRaw(traits["additionalAppointments"])
|
||||
if len(appointments) == 0 {
|
||||
if metadata, ok := traits["metadata"].(map[string]any); ok {
|
||||
appointments = userAppointmentSliceFromRaw(metadata["additionalAppointments"])
|
||||
}
|
||||
}
|
||||
for _, raw := range appointments {
|
||||
appointment, ok := raw.(map[string]any)
|
||||
if !ok || !userAppointmentMatchesTenant(appointment, tenantID, tenantSlug) {
|
||||
continue
|
||||
}
|
||||
if grade := normalizeMetadataString(appointment["grade"]); grade != "" {
|
||||
return grade
|
||||
}
|
||||
}
|
||||
for _, raw := range appointments {
|
||||
appointment, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if isPrimary, ok := metadataBoolFromMap(appointment, "isPrimary", "primary", "representative", "isRepresentative"); !ok || !isPrimary {
|
||||
continue
|
||||
}
|
||||
if grade := normalizeMetadataString(appointment["grade"]); grade != "" {
|
||||
return grade
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func tenantBoundGradeFromUser(user domain.User) string {
|
||||
if user.Metadata == nil {
|
||||
return ""
|
||||
}
|
||||
traits := map[string]any{
|
||||
"tenant_id": userTenantIDValue(user),
|
||||
}
|
||||
for key, value := range user.Metadata {
|
||||
traits[key] = value
|
||||
}
|
||||
return tenantBoundGradeFromTraits(traits)
|
||||
}
|
||||
|
||||
func userTenantIDValue(user domain.User) string {
|
||||
if user.TenantID == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*user.TenantID)
|
||||
}
|
||||
|
||||
func applyTenantBoundGrade(metadata map[string]any, tenant *domain.Tenant, grade string) map[string]any {
|
||||
if metadata == nil {
|
||||
metadata = map[string]any{}
|
||||
}
|
||||
if tenant == nil || strings.TrimSpace(tenant.ID) == "" {
|
||||
return metadata
|
||||
}
|
||||
normalizedGrade := normalizeInternalGradeName(grade)
|
||||
appointments := userAppointmentSliceFromRaw(metadata["additionalAppointments"])
|
||||
matched := false
|
||||
for i, raw := range appointments {
|
||||
appointment, ok := raw.(map[string]any)
|
||||
if !ok || !userAppointmentMatchesTenant(appointment, tenant.ID, tenant.Slug) {
|
||||
continue
|
||||
}
|
||||
appointment["tenantId"] = tenant.ID
|
||||
appointment["tenantSlug"] = tenant.Slug
|
||||
appointment["tenantName"] = tenant.Name
|
||||
if normalizedGrade == "" {
|
||||
delete(appointment, "grade")
|
||||
} else {
|
||||
appointment["grade"] = normalizedGrade
|
||||
}
|
||||
appointments[i] = appointment
|
||||
matched = true
|
||||
}
|
||||
if !matched && normalizedGrade != "" {
|
||||
appointments = append(appointments, map[string]any{
|
||||
"tenantId": tenant.ID,
|
||||
"tenantSlug": tenant.Slug,
|
||||
"tenantName": tenant.Name,
|
||||
"grade": normalizedGrade,
|
||||
})
|
||||
}
|
||||
if len(appointments) > 0 {
|
||||
metadata["additionalAppointments"] = appointments
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func applyTenantBoundGradeToTraits(ctx context.Context, tenantService service.TenantService, traits map[string]any, grade string) {
|
||||
delete(traits, "grade")
|
||||
tenantID := extractTraitString(traits, "tenant_id")
|
||||
tenantSlug := extractTraitString(traits, "tenantSlug")
|
||||
var tenant *domain.Tenant
|
||||
if tenantService != nil {
|
||||
if tenantID != "" {
|
||||
if found, err := tenantService.GetTenant(ctx, tenantID); err == nil {
|
||||
tenant = found
|
||||
}
|
||||
}
|
||||
if tenant == nil && tenantSlug != "" {
|
||||
if found, err := tenantService.GetTenantBySlug(ctx, tenantSlug); err == nil {
|
||||
tenant = found
|
||||
}
|
||||
}
|
||||
}
|
||||
if tenant == nil {
|
||||
tenant = &domain.Tenant{ID: tenantID, Slug: tenantSlug}
|
||||
}
|
||||
if strings.TrimSpace(tenant.ID) == "" && strings.TrimSpace(tenant.Slug) == "" {
|
||||
return
|
||||
}
|
||||
metadata := map[string]any{}
|
||||
if appointments, ok := traits["additionalAppointments"]; ok {
|
||||
metadata["additionalAppointments"] = appointments
|
||||
}
|
||||
metadata = applyTenantBoundGrade(metadata, tenant, grade)
|
||||
if appointments, ok := metadata["additionalAppointments"]; ok {
|
||||
traits["additionalAppointments"] = appointments
|
||||
}
|
||||
}
|
||||
|
||||
func mergeUserAddTenantAppointment(traits map[string]any, metadata map[string]any, tenant *domain.Tenant) map[string]any {
|
||||
if tenant == nil {
|
||||
return metadata
|
||||
@@ -507,14 +675,7 @@ func normalizeAssignableSystemRole(value string) (string, bool) {
|
||||
}
|
||||
|
||||
func gradeFromTraits(traits map[string]any) string {
|
||||
value := strings.TrimSpace(extractTraitString(traits, "grade"))
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if _, ok := domain.NormalizeRoleAlias(value); ok {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
return tenantBoundGradeFromTraits(traits)
|
||||
}
|
||||
|
||||
func rejectLegacyCompanyCode(value string) error {
|
||||
@@ -631,6 +792,14 @@ type identityMirrorLister interface {
|
||||
ListIdentityMirrors(ctx context.Context) ([]service.KratosIdentity, error)
|
||||
}
|
||||
|
||||
type identityMirrorPageLister interface {
|
||||
ListIdentityMirrorPage(ctx context.Context, query service.IdentityMirrorPageQuery) (service.IdentityMirrorPageResult, error)
|
||||
}
|
||||
|
||||
type identityMirrorStore interface {
|
||||
StoreIdentityMirror(ctx context.Context, identity service.KratosIdentity) error
|
||||
}
|
||||
|
||||
type identityMirrorStatusReader interface {
|
||||
GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error)
|
||||
}
|
||||
@@ -856,68 +1025,29 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
identities, err := h.listIdentitiesFromMirrorOrKratos(c.Context())
|
||||
allowedTenantKeys := map[string]bool(nil)
|
||||
if requesterRole != domain.RoleSuperAdmin {
|
||||
allowedTenantKeys = manageableSlugs
|
||||
}
|
||||
page, err := h.listIdentityMirrorPageOrKratos(c.Context(), service.IdentityMirrorPageQuery{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Cursor: cursorRaw,
|
||||
Search: search,
|
||||
TenantSlug: tenantSlug,
|
||||
TenantID: targetTenantID,
|
||||
AllowedTenantKeys: allowedTenantKeys,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("Identity mirror unavailable for user list", "error", err)
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "identity mirror unavailable")
|
||||
}
|
||||
|
||||
filtered := make([]service.KratosIdentity, 0, len(identities))
|
||||
searchLower := strings.ToLower(search)
|
||||
|
||||
for _, identity := range identities {
|
||||
tenantAccessKeys := identityTenantAccessKeys(identity.Traits)
|
||||
|
||||
// Tenant Admin & Member filtering
|
||||
if requesterRole != domain.RoleSuperAdmin {
|
||||
hasAccess := anyTenantKeyAllowed(tenantAccessKeys, manageableSlugs)
|
||||
if !hasAccess {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated tenantSlug filter
|
||||
if tenantSlug != "" {
|
||||
targetKeys := map[string]bool{
|
||||
targetTenantID: true,
|
||||
strings.ToLower(tenantSlug): true,
|
||||
}
|
||||
matches := anyTenantKeyAllowed(tenantAccessKeys, targetKeys)
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !identityMatchesSearch(identity, searchLower) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, identity)
|
||||
}
|
||||
|
||||
pagination.SortByKeyDesc(filtered, kratosIdentityCursorKey)
|
||||
total := int64(len(filtered))
|
||||
nextCursor := ""
|
||||
var pageIdentities []service.KratosIdentity
|
||||
if cursorRaw != "" {
|
||||
pageIdentities, nextCursor, err = pagination.PageByCursor(filtered, limit, cursorRaw, kratosIdentityCursorKey)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
|
||||
}
|
||||
offset = 0
|
||||
} else {
|
||||
if offset > len(filtered) {
|
||||
offset = len(filtered)
|
||||
}
|
||||
end := min(offset+limit, len(filtered))
|
||||
pageIdentities = filtered[offset:end]
|
||||
if total > int64(end) && len(pageIdentities) > 0 {
|
||||
lastTimestamp, lastID := kratosIdentityCursorKey(pageIdentities[len(pageIdentities)-1])
|
||||
nextCursor = pagination.Encode(lastTimestamp, lastID)
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]userSummary, 0, len(pageIdentities))
|
||||
for _, identity := range pageIdentities {
|
||||
items := make([]userSummary, 0, len(page.Items))
|
||||
for _, identity := range page.Items {
|
||||
summary := h.mapIdentitySummary(c.Context(), identity)
|
||||
items = append(items, summary)
|
||||
}
|
||||
@@ -926,9 +1056,9 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
Items: items,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: total,
|
||||
Total: page.Total,
|
||||
Cursor: cursorRaw,
|
||||
NextCursor: nextCursor,
|
||||
NextCursor: page.NextCursor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1024,6 +1154,84 @@ func (h *UserHandler) listIdentitiesFromMirrorOrKratos(ctx context.Context) ([]s
|
||||
return h.rebuildIdentityMirror(ctx)
|
||||
}
|
||||
|
||||
func (h *UserHandler) listIdentityMirrorPageOrKratos(ctx context.Context, query service.IdentityMirrorPageQuery) (service.IdentityMirrorPageResult, error) {
|
||||
if h != nil && h.IdentityCache != nil {
|
||||
if lister, ok := h.IdentityCache.(identityMirrorPageLister); ok && h.identityMirrorStatusReady(ctx) {
|
||||
return lister.ListIdentityMirrorPage(ctx, query)
|
||||
}
|
||||
}
|
||||
|
||||
identities, err := h.rebuildIdentityMirror(ctx)
|
||||
if err != nil {
|
||||
return service.IdentityMirrorPageResult{}, err
|
||||
}
|
||||
if h != nil && h.IdentityCache != nil {
|
||||
if lister, ok := h.IdentityCache.(identityMirrorPageLister); ok && h.identityMirrorStatusReady(ctx) {
|
||||
return lister.ListIdentityMirrorPage(ctx, query)
|
||||
}
|
||||
}
|
||||
return pageIdentityMirrorSlice(identities, query)
|
||||
}
|
||||
|
||||
func pageIdentityMirrorSlice(identities []service.KratosIdentity, query service.IdentityMirrorPageQuery) (service.IdentityMirrorPageResult, error) {
|
||||
if query.Limit <= 0 {
|
||||
query.Limit = 50
|
||||
}
|
||||
if query.Offset < 0 {
|
||||
query.Offset = 0
|
||||
}
|
||||
searchLower := strings.ToLower(strings.TrimSpace(query.Search))
|
||||
targetKeys := make(map[string]bool)
|
||||
for _, value := range []string{query.TenantID, query.TenantSlug} {
|
||||
key := strings.ToLower(strings.TrimSpace(value))
|
||||
if key != "" {
|
||||
targetKeys[key] = true
|
||||
}
|
||||
}
|
||||
filtered := make([]service.KratosIdentity, 0, len(identities))
|
||||
for _, identity := range identities {
|
||||
tenantAccessKeys := identityTenantAccessKeys(identity.Traits)
|
||||
if len(query.AllowedTenantKeys) > 0 && !anyTenantKeyAllowed(tenantAccessKeys, query.AllowedTenantKeys) {
|
||||
continue
|
||||
}
|
||||
if len(targetKeys) > 0 && !anyTenantKeyAllowed(tenantAccessKeys, targetKeys) {
|
||||
continue
|
||||
}
|
||||
if !identityMatchesSearch(identity, searchLower) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, identity)
|
||||
}
|
||||
|
||||
pagination.SortByKeyDesc(filtered, kratosIdentityCursorKey)
|
||||
total := int64(len(filtered))
|
||||
nextCursor := ""
|
||||
var pageIdentities []service.KratosIdentity
|
||||
if strings.TrimSpace(query.Cursor) != "" {
|
||||
var err error
|
||||
pageIdentities, nextCursor, err = pagination.PageByCursor(filtered, query.Limit, query.Cursor, kratosIdentityCursorKey)
|
||||
if err != nil {
|
||||
return service.IdentityMirrorPageResult{}, err
|
||||
}
|
||||
} else {
|
||||
if query.Offset > len(filtered) {
|
||||
query.Offset = len(filtered)
|
||||
}
|
||||
end := min(query.Offset+query.Limit, len(filtered))
|
||||
pageIdentities = filtered[query.Offset:end]
|
||||
if total > int64(end) && len(pageIdentities) > 0 {
|
||||
lastTimestamp, lastID := kratosIdentityCursorKey(pageIdentities[len(pageIdentities)-1])
|
||||
nextCursor = pagination.Encode(lastTimestamp, lastID)
|
||||
}
|
||||
}
|
||||
return service.IdentityMirrorPageResult{
|
||||
Items: pageIdentities,
|
||||
Total: total,
|
||||
Cursor: query.Cursor,
|
||||
NextCursor: nextCursor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) WarmIdentityMirror(ctx context.Context) (int, error) {
|
||||
identities, err := h.rebuildIdentityMirror(ctx)
|
||||
if err != nil {
|
||||
@@ -1066,6 +1274,24 @@ func (h *UserHandler) identityMirrorReady(ctx context.Context, identityCount int
|
||||
status.ObservedCount == int64(identityCount)
|
||||
}
|
||||
|
||||
func (h *UserHandler) identityMirrorStatusReady(ctx context.Context) bool {
|
||||
if h == nil || h.IdentityCache == nil {
|
||||
return false
|
||||
}
|
||||
reader, ok := h.IdentityCache.(identityMirrorStatusReader)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
status, err := reader.GetIdentityCacheStatus(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return status.RedisReady &&
|
||||
status.Status == "ready" &&
|
||||
status.MirrorVersion == identityMirrorVersion &&
|
||||
status.ObservedCount > 0
|
||||
}
|
||||
|
||||
func (h *UserHandler) flushIdentityMirror(ctx context.Context) {
|
||||
if h == nil || h.IdentityCache == nil {
|
||||
return
|
||||
@@ -1108,6 +1334,10 @@ func (h *UserHandler) storeIdentityMirror(identity service.KratosIdentity) {
|
||||
if h == nil || h.IdentityCache == nil || strings.TrimSpace(identity.ID) == "" {
|
||||
return
|
||||
}
|
||||
if store, ok := h.IdentityCache.(identityMirrorStore); ok {
|
||||
_ = store.StoreIdentityMirror(context.Background(), identity)
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(identity)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -1235,7 +1465,6 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
|
||||
attributes := map[string]any{
|
||||
"department": req.Department,
|
||||
"grade": strings.TrimSpace(req.Grade),
|
||||
"position": req.Position,
|
||||
"jobTitle": req.JobTitle,
|
||||
"affiliationType": "internal",
|
||||
@@ -1289,6 +1518,7 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
tenantID = tenant.ID
|
||||
req.CompanyCode = tenant.Slug
|
||||
resolvedTenant = tenant
|
||||
}
|
||||
|
||||
attributes["role"] = role
|
||||
@@ -1308,6 +1538,10 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Grade) != "" {
|
||||
req.Metadata = applyTenantBoundGrade(req.Metadata, resolvedTenant, req.Grade)
|
||||
}
|
||||
|
||||
// Merge custom metadata into attributes
|
||||
for k, v := range req.Metadata {
|
||||
// Don't overwrite core fields
|
||||
@@ -1789,6 +2023,13 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
item.Metadata["additionalAppointments"] = resolvedAppointments
|
||||
}
|
||||
if strings.TrimSpace(item.Grade) != "" {
|
||||
item.Metadata = applyTenantBoundGrade(item.Metadata, &domain.Tenant{
|
||||
ID: tItem.ID,
|
||||
Slug: tItem.Slug,
|
||||
Name: tItem.Name,
|
||||
}, item.Grade)
|
||||
}
|
||||
normalizeBulkUserAliasMetadata(item.Metadata)
|
||||
item.Metadata = sanitizeUserMetadata(item.Metadata)
|
||||
|
||||
@@ -1800,7 +2041,6 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
|
||||
attributes := map[string]any{
|
||||
"department": dept,
|
||||
"grade": strings.TrimSpace(item.Grade),
|
||||
"position": strings.TrimSpace(item.Position),
|
||||
"jobTitle": strings.TrimSpace(item.JobTitle),
|
||||
"affiliationType": "internal",
|
||||
@@ -2350,7 +2590,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
traits["department"] = *req.Department
|
||||
}
|
||||
if req.Grade != nil {
|
||||
traits["grade"] = *req.Grade
|
||||
applyTenantBoundGradeToTraits(c.Context(), h.TenantService, traits, *req.Grade)
|
||||
}
|
||||
if req.Position != nil {
|
||||
traits["position"] = *req.Position
|
||||
@@ -2783,7 +3023,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
traits["department"] = strings.TrimSpace(*req.Department)
|
||||
}
|
||||
if req.Grade != nil {
|
||||
traits["grade"] = strings.TrimSpace(*req.Grade)
|
||||
applyTenantBoundGradeToTraits(c.Context(), h.TenantService, traits, *req.Grade)
|
||||
}
|
||||
if req.Position != nil {
|
||||
traits["position"] = strings.TrimSpace(*req.Position)
|
||||
@@ -3344,7 +3584,7 @@ func (h *UserHandler) mapLocalUserSummary(ctx context.Context, user domain.User)
|
||||
TenantSlug: tenantSlug,
|
||||
CompanyCode: tenantSlug,
|
||||
Department: user.Department,
|
||||
Grade: user.Grade,
|
||||
Grade: tenantBoundGradeFromUser(user),
|
||||
Position: user.Position,
|
||||
JobTitle: user.JobTitle,
|
||||
Metadata: user.Metadata,
|
||||
|
||||
Reference in New Issue
Block a user