forked from baron/baron-sso
kratos SSOT 재설계
This commit is contained in:
@@ -24,7 +24,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// OryProviderAPI defines the subset of Ory Provider used by UserHandler
|
||||
@@ -44,6 +43,7 @@ type UserHandler struct {
|
||||
UserProjectionRepo repository.UserProjectionRepository
|
||||
UserGroupRepo repository.UserGroupRepository
|
||||
AuditRepo domain.AuditRepository
|
||||
IdentityCache domain.RedisRepository
|
||||
Worksmobile service.WorksmobileSyncer
|
||||
}
|
||||
|
||||
@@ -589,6 +589,24 @@ func profileTenantAccessKeys(profile *domain.UserProfileResponse) map[string]boo
|
||||
return allowed
|
||||
}
|
||||
|
||||
func identityMirrorKey(identityID string) string {
|
||||
return "identity:mirror:" + strings.TrimSpace(identityID)
|
||||
}
|
||||
|
||||
type identityMirrorLister interface {
|
||||
ListIdentityMirrors(ctx context.Context) ([]service.KratosIdentity, error)
|
||||
}
|
||||
|
||||
type identityMirrorStatusReader interface {
|
||||
GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error)
|
||||
}
|
||||
|
||||
type identityMirrorFlusher interface {
|
||||
FlushIdentityCache(ctx context.Context) (domain.IdentityCacheFlushResult, error)
|
||||
}
|
||||
|
||||
const identityMirrorVersion = "kratos-full-pagination-v1"
|
||||
|
||||
func profileCanAccessTenant(profile *domain.UserProfileResponse, tenantID, tenantSlug string) bool {
|
||||
allowed := profileTenantAccessKeys(profile)
|
||||
if id := strings.ToLower(strings.TrimSpace(tenantID)); id != "" && allowed[id] {
|
||||
@@ -654,6 +672,26 @@ func kratosIdentityCursorKey(identity service.KratosIdentity) (time.Time, string
|
||||
return timestamp, identity.ID
|
||||
}
|
||||
|
||||
func identityMatchesSearch(identity service.KratosIdentity, searchLower string) bool {
|
||||
if searchLower == "" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(identity.ID), searchLower) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(extractTraitString(identity.Traits, "email")), searchLower) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(extractTraitString(identity.Traits, "name")), searchLower) {
|
||||
return true
|
||||
}
|
||||
rawTraits, err := json.Marshal(identity.Traits)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(strings.ToLower(string(rawTraits)), searchLower)
|
||||
}
|
||||
|
||||
func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
// [New] Get requester profile from middleware
|
||||
var requesterRole string
|
||||
@@ -745,161 +783,96 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
if h.UserRepo != nil {
|
||||
var tenantIDs []string
|
||||
if tenantSlug != "" && targetTenantID == "" {
|
||||
return c.JSON(userListResponse{
|
||||
Items: []userSummary{},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: 0,
|
||||
Cursor: cursorRaw,
|
||||
})
|
||||
}
|
||||
|
||||
if requesterRole != domain.RoleSuperAdmin && tenantSlug != "" && !manageableSlugs[targetTenantID] && !manageableSlugs[strings.ToLower(tenantSlug)] {
|
||||
return c.JSON(userListResponse{
|
||||
Items: []userSummary{},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: 0,
|
||||
Cursor: cursorRaw,
|
||||
})
|
||||
}
|
||||
|
||||
identities, err := h.listIdentitiesFromMirrorOrKratos(c.Context())
|
||||
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 {
|
||||
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
|
||||
|
||||
// Tenant Admin & Member filtering
|
||||
if requesterRole != domain.RoleSuperAdmin {
|
||||
hasAccess := manageableSlugs[tID]
|
||||
if !hasAccess {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated tenantSlug filter
|
||||
if tenantSlug != "" {
|
||||
if targetTenantID == "" {
|
||||
return c.JSON(userListResponse{
|
||||
Items: []userSummary{},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: 0,
|
||||
Cursor: cursorRaw,
|
||||
})
|
||||
}
|
||||
if requesterRole != domain.RoleSuperAdmin && !manageableSlugs[targetTenantID] && !manageableSlugs[strings.ToLower(tenantSlug)] {
|
||||
return c.JSON(userListResponse{
|
||||
Items: []userSummary{},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: 0,
|
||||
Cursor: cursorRaw,
|
||||
})
|
||||
}
|
||||
tenantIDs = append(tenantIDs, targetTenantID)
|
||||
} else if requesterRole != domain.RoleSuperAdmin {
|
||||
for key := range manageableSlugs {
|
||||
if _, err := uuid.Parse(key); err == nil {
|
||||
tenantIDs = append(tenantIDs, key)
|
||||
}
|
||||
}
|
||||
if len(tenantIDs) == 0 {
|
||||
return c.JSON(userListResponse{
|
||||
Items: []userSummary{},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: 0,
|
||||
Cursor: cursorRaw,
|
||||
})
|
||||
matches := tID == targetTenantID
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
users, total, nextCursor, err := h.UserRepo.List(c.Context(), offset, limit, search, tenantIDs, cursorRaw)
|
||||
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.StatusInternalServerError, "failed to list users")
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid cursor")
|
||||
}
|
||||
items := make([]userSummary, 0, len(users))
|
||||
for _, user := range users {
|
||||
items = append(items, h.mapLocalUserSummary(c.Context(), user))
|
||||
offset = 0
|
||||
} else {
|
||||
if offset > len(filtered) {
|
||||
offset = len(filtered)
|
||||
}
|
||||
if cursorRaw != "" {
|
||||
offset = 0
|
||||
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)
|
||||
}
|
||||
return c.JSON(userListResponse{
|
||||
Items: items,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: total,
|
||||
Cursor: cursorRaw,
|
||||
NextCursor: nextCursor,
|
||||
})
|
||||
}
|
||||
|
||||
if h.KratosAdmin == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
|
||||
items := make([]userSummary, 0, len(pageIdentities))
|
||||
for _, identity := range pageIdentities {
|
||||
summary := h.mapIdentitySummary(c.Context(), identity)
|
||||
items = append(items, summary)
|
||||
}
|
||||
|
||||
identities, err := h.KratosAdmin.ListIdentities(c.Context())
|
||||
if err == nil {
|
||||
filtered := make([]service.KratosIdentity, 0, len(identities))
|
||||
searchLower := strings.ToLower(search)
|
||||
|
||||
for _, identity := range identities {
|
||||
email := strings.ToLower(extractTraitString(identity.Traits, "email"))
|
||||
name := strings.ToLower(extractTraitString(identity.Traits, "name"))
|
||||
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
|
||||
|
||||
// Tenant Admin & Member filtering
|
||||
if requesterRole != domain.RoleSuperAdmin {
|
||||
hasAccess := manageableSlugs[tID]
|
||||
if !hasAccess {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Dedicated tenantSlug filter
|
||||
if tenantSlug != "" {
|
||||
matches := tID == targetTenantID
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Search filtering
|
||||
if search != "" {
|
||||
matchesSearch := strings.Contains(email, searchLower) ||
|
||||
strings.Contains(name, searchLower)
|
||||
|
||||
if !matchesSearch {
|
||||
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 {
|
||||
summary := h.mapIdentitySummary(c.Context(), identity)
|
||||
items = append(items, summary)
|
||||
}
|
||||
|
||||
// [Lazy Sync] Asynchronously update local DB with fresh data from Kratos
|
||||
// This ensures that member counts (which use local DB) eventually match reality
|
||||
if h.UserRepo != nil {
|
||||
go func(ids []service.KratosIdentity) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
for _, identity := range ids {
|
||||
localUser := h.mapToLocalUser(identity)
|
||||
_ = h.UserRepo.Update(ctx, localUser)
|
||||
}
|
||||
}(filtered)
|
||||
}
|
||||
|
||||
return c.JSON(userListResponse{
|
||||
Items: items,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: total,
|
||||
Cursor: cursorRaw,
|
||||
NextCursor: nextCursor,
|
||||
})
|
||||
}
|
||||
|
||||
slog.Warn("Kratos unavailable for user list", "error", err)
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider unavailable")
|
||||
return c.JSON(userListResponse{
|
||||
Items: items,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: total,
|
||||
Cursor: cursorRaw,
|
||||
NextCursor: nextCursor,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
||||
@@ -912,26 +885,30 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "user id is required")
|
||||
}
|
||||
|
||||
if identity := h.getIdentityFromMirror(userID); identity != nil {
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if requester != nil && requester.Role != domain.RoleSuperAdmin {
|
||||
allowedKeys := profileTenantAccessKeys(requester)
|
||||
if !anyTenantKeyAllowed(identityTenantAccessKeys(identity.Traits), allowedKeys) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access to user in another tenant denied")
|
||||
}
|
||||
}
|
||||
return c.JSON(h.mapIdentitySummary(c.Context(), *identity))
|
||||
}
|
||||
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
||||
if err != nil || identity == nil {
|
||||
// [FIX] Support fixed UUID lookup fallback
|
||||
id, searchErr := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), userID)
|
||||
if searchErr == nil && id != "" {
|
||||
identity, err = h.KratosAdmin.GetIdentity(c.Context(), id)
|
||||
}
|
||||
|
||||
if err != nil || identity == nil {
|
||||
// Second Fallback: By Email from local DB
|
||||
if h.UserRepo != nil {
|
||||
local, _ := h.UserRepo.FindByID(c.Context(), userID)
|
||||
if local != nil && local.Email != "" {
|
||||
id, _ = h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), local.Email)
|
||||
if id != "" {
|
||||
identity, err = h.KratosAdmin.GetIdentity(c.Context(), id)
|
||||
}
|
||||
}
|
||||
if cached := h.getIdentityFromMirror(id); cached != nil {
|
||||
identity = cached
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if searchErr == nil && id != "" && (err != nil || identity == nil) {
|
||||
identity, err = h.KratosAdmin.GetIdentity(c.Context(), id)
|
||||
}
|
||||
|
||||
if err != nil || identity == nil {
|
||||
if identity == nil {
|
||||
@@ -940,6 +917,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
h.storeIdentityMirror(*identity)
|
||||
|
||||
// [New] Check access scope
|
||||
requester, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
@@ -953,6 +931,149 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
|
||||
return c.JSON(h.mapIdentitySummary(c.Context(), *identity))
|
||||
}
|
||||
|
||||
func (h *UserHandler) getIdentityFromMirror(identityID string) *service.KratosIdentity {
|
||||
if h == nil || h.IdentityCache == nil {
|
||||
return nil
|
||||
}
|
||||
raw, err := h.IdentityCache.Get(identityMirrorKey(identityID))
|
||||
if err != nil || strings.TrimSpace(raw) == "" {
|
||||
return nil
|
||||
}
|
||||
var identity service.KratosIdentity
|
||||
if err := json.Unmarshal([]byte(raw), &identity); err != nil {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(identity.ID) == "" {
|
||||
return nil
|
||||
}
|
||||
return &identity
|
||||
}
|
||||
|
||||
func (h *UserHandler) listIdentitiesFromMirrorOrKratos(ctx context.Context) ([]service.KratosIdentity, error) {
|
||||
if h != nil && h.IdentityCache != nil {
|
||||
if lister, ok := h.IdentityCache.(identityMirrorLister); ok {
|
||||
identities, err := lister.ListIdentityMirrors(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.identityMirrorReady(ctx, len(identities)) {
|
||||
return identities, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if h == nil || h.KratosAdmin == nil {
|
||||
return nil, errors.New("identity mirror is empty and kratos admin service is unavailable")
|
||||
}
|
||||
return h.rebuildIdentityMirror(ctx)
|
||||
}
|
||||
|
||||
func (h *UserHandler) WarmIdentityMirror(ctx context.Context) (int, error) {
|
||||
identities, err := h.rebuildIdentityMirror(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(identities), nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) rebuildIdentityMirror(ctx context.Context) ([]service.KratosIdentity, error) {
|
||||
if h == nil || h.KratosAdmin == nil {
|
||||
return nil, errors.New("kratos admin service is unavailable")
|
||||
}
|
||||
identities, err := h.KratosAdmin.ListIdentities(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.flushIdentityMirror(ctx)
|
||||
for _, identity := range identities {
|
||||
h.storeIdentityMirror(identity)
|
||||
}
|
||||
h.markIdentityMirrorReady(len(identities))
|
||||
return identities, nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) identityMirrorReady(ctx context.Context, identityCount int) bool {
|
||||
if h == nil || h.IdentityCache == nil || identityCount == 0 {
|
||||
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 == int64(identityCount)
|
||||
}
|
||||
|
||||
func (h *UserHandler) flushIdentityMirror(ctx context.Context) {
|
||||
if h == nil || h.IdentityCache == nil {
|
||||
return
|
||||
}
|
||||
flusher, ok := h.IdentityCache.(identityMirrorFlusher)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_, _ = flusher.FlushIdentityCache(ctx)
|
||||
}
|
||||
|
||||
func (h *UserHandler) markIdentityMirrorReady(identityCount int) {
|
||||
if h == nil || h.IdentityCache == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
status := domain.IdentityCacheStatus{
|
||||
Status: "ready",
|
||||
RedisReady: true,
|
||||
MirrorVersion: identityMirrorVersion,
|
||||
ObservedCount: int64(identityCount),
|
||||
LastRefreshedAt: &now,
|
||||
UpdatedAt: &now,
|
||||
}
|
||||
raw, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = h.IdentityCache.Set("identity:mirror:state", string(raw), 0)
|
||||
}
|
||||
|
||||
func (h *UserHandler) invalidateIdentityMirrorState() {
|
||||
if h == nil || h.IdentityCache == nil {
|
||||
return
|
||||
}
|
||||
_ = h.IdentityCache.Delete("identity:mirror:state")
|
||||
}
|
||||
|
||||
func (h *UserHandler) storeIdentityMirror(identity service.KratosIdentity) {
|
||||
if h == nil || h.IdentityCache == nil || strings.TrimSpace(identity.ID) == "" {
|
||||
return
|
||||
}
|
||||
raw, err := json.Marshal(identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = h.IdentityCache.Set(identityMirrorKey(identity.ID), string(raw), 0)
|
||||
}
|
||||
|
||||
func (h *UserHandler) updateIdentityMirrorEntry(identity service.KratosIdentity) {
|
||||
h.storeIdentityMirror(identity)
|
||||
h.invalidateIdentityMirrorState()
|
||||
}
|
||||
|
||||
func (h *UserHandler) deleteIdentityMirrorEntry(identityID string) {
|
||||
if h == nil || h.IdentityCache == nil {
|
||||
return
|
||||
}
|
||||
identityID = strings.TrimSpace(identityID)
|
||||
if identityID != "" {
|
||||
_ = h.IdentityCache.Delete(identityMirrorKey(identityID))
|
||||
}
|
||||
h.invalidateIdentityMirrorState()
|
||||
}
|
||||
|
||||
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
if h.OryProvider == nil || h.KratosAdmin == nil {
|
||||
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
|
||||
@@ -1171,8 +1292,10 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if identity == nil {
|
||||
h.invalidateIdentityMirrorState()
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": identityID, "initialPassword": generatedPassword})
|
||||
}
|
||||
h.updateIdentityMirrorEntry(*identity)
|
||||
|
||||
// [New] Local DB Sync - Ensure user exists in read-model
|
||||
if h.UserRepo != nil {
|
||||
@@ -1672,6 +1795,7 @@ func (h *UserHandler) BulkCreateUsers(c *fiber.Ctx) error {
|
||||
}
|
||||
} else {
|
||||
resultStatus = "created"
|
||||
h.invalidateIdentityMirrorState()
|
||||
slog.Info("BulkCreate: New identity created", "email", userEmail, "identityID", identityID)
|
||||
}
|
||||
}
|
||||
@@ -2160,6 +2284,7 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
|
||||
continue
|
||||
}
|
||||
h.updateIdentityMirrorEntry(*updated)
|
||||
|
||||
// Sync to local DB
|
||||
if h.UserRepo != nil {
|
||||
@@ -2267,6 +2392,7 @@ func (h *UserHandler) BulkDeleteUsers(c *fiber.Ctx) error {
|
||||
results = append(results, map[string]any{"id": id, "success": false, "message": err.Error()})
|
||||
continue
|
||||
}
|
||||
h.deleteIdentityMirrorEntry(id)
|
||||
if h.Worksmobile != nil {
|
||||
localUser := h.mapToLocalUser(*identity)
|
||||
if err := h.Worksmobile.EnqueueUserDeleteIfInScope(c.Context(), *localUser); err != nil {
|
||||
@@ -2635,6 +2761,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
|
||||
}
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
h.updateIdentityMirrorEntry(*updated)
|
||||
|
||||
// [New] Local DB Sync - Sync synchronously to ensure immediate consistency for the caller
|
||||
if h.UserRepo != nil {
|
||||
@@ -2807,6 +2934,10 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
h.deleteIdentityMirrorEntry(userID)
|
||||
if actualKratosID != userID {
|
||||
h.deleteIdentityMirrorEntry(actualKratosID)
|
||||
}
|
||||
slog.Info("[UserHandler] Successfully deleted Kratos identity", "userID", userID, "actualKratosID", actualKratosID)
|
||||
|
||||
if h.Worksmobile != nil && identity != nil {
|
||||
@@ -3003,16 +3134,6 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
|
||||
traits := identity.Traits
|
||||
role := roleFromTraits(traits)
|
||||
|
||||
// [FIX] Prioritize Local DB ID (the fixed UUID from user)
|
||||
finalID := identity.ID
|
||||
email := extractTraitString(traits, "email")
|
||||
if h.UserRepo != nil && email != "" {
|
||||
// 1. Try finding by email first as it's a strong identifier
|
||||
if local, err := h.UserRepo.FindByEmail(ctx, email); err == nil && local != nil {
|
||||
finalID = local.ID
|
||||
}
|
||||
}
|
||||
|
||||
tenantID := extractTraitString(traits, "tenant_id")
|
||||
tenantSlug := ""
|
||||
var tenantSummary *domain.Tenant
|
||||
@@ -3038,7 +3159,7 @@ func (h *UserHandler) mapIdentitySummary(ctx context.Context, identity service.K
|
||||
}
|
||||
|
||||
summary := userSummary{
|
||||
ID: finalID,
|
||||
ID: identity.ID,
|
||||
Email: extractTraitString(traits, "email"),
|
||||
LoginID: resolvePasswordLoginID(traits),
|
||||
CustomLoginIDs: customLoginIDs,
|
||||
|
||||
Reference in New Issue
Block a user