From 7a057fa5dbe972bb6c2da576c1776b588ce6955f Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 6 Feb 2026 11:23:52 +0900 Subject: [PATCH] =?UTF-8?q?DevHandler=20=EB=8F=99=EC=9D=98=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20API=EB=A5=BC=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=20DB=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/dev_handler.go | 137 ++++++++++++++---------- 1 file changed, 81 insertions(+), 56 deletions(-) diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index 33a9b3f5..18d8347d 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -2,6 +2,7 @@ package handler import ( "baron-sso-backend/internal/domain" + "baron-sso-backend/internal/repository" "baron-sso-backend/internal/service" "context" "errors" @@ -17,14 +18,16 @@ type DevHandler struct { Redis *service.RedisService SecretRepo domain.ClientSecretRepository KratosAdmin *service.KratosAdminService + ConsentRepo repository.ClientConsentRepository } -func NewDevHandler(redis *service.RedisService, secretRepo domain.ClientSecretRepository) *DevHandler { +func NewDevHandler(redis *service.RedisService, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository) *DevHandler { return &DevHandler{ Hydra: service.NewHydraAdminService(), Redis: redis, SecretRepo: secretRepo, KratosAdmin: service.NewKratosAdminService(), + ConsentRepo: consentRepo, } } @@ -60,12 +63,15 @@ type clientEndpoints struct { } type consentSummary struct { - Subject string `json:"subject"` - UserName string `json:"userName,omitempty"` - ClientID string `json:"clientId"` - ClientName string `json:"clientName,omitempty"` - GrantedScopes []string `json:"grantedScopes"` - AuthenticatedAt string `json:"authenticatedAt,omitempty"` + Subject string `json:"subject"` + UserName string `json:"userName,omitempty"` + ClientID string `json:"clientId"` + ClientName string `json:"clientName,omitempty"` + GrantedScopes []string `json:"grantedScopes"` + AuthenticatedAt string `json:"authenticatedAt,omitempty"` + CreatedAt time.Time `json:"createdAt"` + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` } type consentListResponse struct { @@ -394,70 +400,84 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error { } func (h *DevHandler) ListConsents(c *fiber.Ctx) error { - subject := strings.TrimSpace(c.Query("subject")) - if subject == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subject is required"}) - } - - // If subject is not a UUID, try to resolve it as an identifier (email/username) - if _, err := uuid.Parse(subject); err != nil { - resolved, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), subject) - if err == nil && resolved != "" { - subject = resolved - } - } - clientID := strings.TrimSpace(c.Query("client_id")) + if clientID == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client_id is required"}) + } + + subject := strings.TrimSpace(c.Query("subject")) + limit := c.QueryInt("limit", 50) + offset := c.QueryInt("offset", 0) + if limit <= 0 { + limit = 50 + } + + // [Isolation] Get admin tenant ID from header or locals + adminTenantID := c.Get("X-Tenant-ID") // Assume middleware sets this or trusted in dev + + var consents []domain.ClientConsentWithTenantInfo + var total int64 + var err error + + if subject != "" { + // Resolve subject if it's email/name (Legacy support) + if _, err := uuid.Parse(subject); err != nil { + resolved, _ := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), subject) + if resolved != "" { + subject = resolved + } + } + + // Single user fetch from Hydra (to get latest status) or Local DB + // Issue says: "List All", so we prefer Local DB for consistency in listing + // But for a single user, we could still use Hydra. + // Let's use Local DB to support tenant filtering even for search. + // For simplicity, we just filter the list later if search is used. + } + + if adminTenantID != "" { + consents, total, err = h.ConsentRepo.ListByTenant(c.Context(), clientID, adminTenantID, limit, offset) + } else { + consents, total, err = h.ConsentRepo.List(c.Context(), clientID, limit, offset) + } - sessions, err := h.Hydra.ListConsentSessions(c.Context(), subject, clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } - items := make([]consentSummary, 0, len(sessions)) - for _, session := range sessions { - client := session.Client - if client.ClientID == "" && session.ConsentRequest != nil { - client = session.ConsentRequest.Client - } - subject := session.Subject - if subject == "" && session.ConsentRequest != nil { - subject = session.ConsentRequest.Subject + items := make([]consentSummary, 0, len(consents)) + for _, consent := range consents { + // Filter by subject if search is active + if subject != "" && consent.Subject != subject { + continue } userName := "" - if subject != "" { - identity, err := h.KratosAdmin.GetIdentity(c.Context(), subject) - if err == nil && identity != nil { - if name, ok := identity.Traits["name"].(string); ok { - userName = name - } else if displayName, ok := identity.Traits["displayname"].(string); ok { - userName = displayName - } else if email, ok := identity.Traits["email"].(string); ok { - userName = email - } + identity, err := h.KratosAdmin.GetIdentity(c.Context(), consent.Subject) + if err == nil && identity != nil { + if name, ok := identity.Traits["name"].(string); ok { + userName = name + } else if email, ok := identity.Traits["email"].(string); ok { + userName = email } } - authAt := "" - if session.AuthenticatedAt != nil { - authAt = session.AuthenticatedAt.Format(time.RFC3339) - } else if session.RequestedAt != nil { - authAt = session.RequestedAt.Format(time.RFC3339) - } else if session.HandledAt != nil { - authAt = session.HandledAt.Format(time.RFC3339) - } items = append(items, consentSummary{ - Subject: subject, + Subject: consent.Subject, UserName: userName, - ClientID: client.ClientID, - ClientName: client.ClientName, - GrantedScopes: session.GrantedScope, - AuthenticatedAt: authAt, + ClientID: consent.ClientID, + GrantedScopes: consent.GrantedScopes, + AuthenticatedAt: consent.UpdatedAt.Format(time.RFC3339), + CreatedAt: consent.CreatedAt, + TenantID: consent.TenantID, + TenantName: consent.TenantName, }) } - return c.JSON(consentListResponse{Items: items}) + return c.JSON(fiber.Map{ + "items": items, + "total": total, + }) } func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error { @@ -465,6 +485,7 @@ func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error { if subject == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subject is required"}) } + clientID := strings.TrimSpace(c.Query("client_id")) // If subject is not a UUID, try to resolve it as an identifier (email/username) if _, err := uuid.Parse(subject); err != nil { @@ -474,12 +495,16 @@ func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error { } } - clientID := strings.TrimSpace(c.Query("client_id")) - + // 1. Revoke in Hydra if err := h.Hydra.RevokeConsentSessions(c.Context(), subject, clientID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } + // 2. Sync to Local DB (Delete) + if h.ConsentRepo != nil { + _ = h.ConsentRepo.Delete(c.Context(), subject, clientID) + } + return c.SendStatus(fiber.StatusNoContent) }