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

@@ -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,