From bb78c8e5725bd014ba7dd9f953dbed5c6aff6d02 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 9 Feb 2026 11:18:32 +0900 Subject: [PATCH] =?UTF-8?q?Linked=20API=EC=97=90=20Audit=20Log=20=EC=8A=A4?= =?UTF-8?q?=EC=BA=94=EC=9D=84=20=ED=86=B5=ED=95=9C=20=EA=B3=BC=EA=B1=B0=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EB=B3=B4=EA=B0=95=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 78 ++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 98d86b10..9941f64d 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -3386,6 +3386,84 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error { } } + // [New] Audit Log Scan for recent history fallback (timeline 200 items) + // Hydra 세션이나 로컬 DB(ConsentRepo)에 없지만 최근 활동 이력이 있는 앱을 보강 + if h.AuditRepo != nil { + for _, subject := range subjects { + auditLogs, err := h.AuditRepo.FindByUserAndEvents(c.Context(), subject, []string{"consent.granted", "consent.revoked"}, 200) + if err != nil { + slog.Error("failed to scan audit logs for linked rps", "error", err, "subject", subject) + continue + } + for _, log := range auditLogs { + var details struct { + ClientID string `json:"client_id"` + ClientName string `json:"client_name"` + Scopes interface{} `json:"scopes"` + } + // 로그 Details 파싱 + if err := json.Unmarshal([]byte(log.Details), &details); err != nil { + continue + } + if details.ClientID == "" { + continue + } + + // 이미 records에 있으면(Active or ConsentRepo) 패스 + if _, exists := records[details.ClientID]; exists { + continue + } + + // 스코프 추출 (consent.granted인 경우) + scopes := []string{} + if sList, ok := details.Scopes.([]interface{}); ok { + for _, s := range sList { + if str, ok := s.(string); ok { + scopes = append(scopes, str) + } + } + } + + // 기본 레코드 생성 + record := &linkedRpRecord{ + linkedRpSummary: linkedRpSummary{ + ID: details.ClientID, + Name: details.ClientName, // revoked 로그일 경우 비어있을 수 있음 + Status: "inactive", + Scopes: scopes, + }, + lastAuth: log.Timestamp, + } + + // Hydra에서 최신 메타데이터 조회 시도 + client, err := h.Hydra.GetClient(c.Context(), details.ClientID) + if err == nil { + name := strings.TrimSpace(client.ClientName) + if name == "" { + name = client.ClientID + } + record.Name = name + record.Logo = extractHydraClientLogo(client.Metadata) + + clientURL := strings.TrimSpace(client.ClientURI) + if clientURL == "" && len(client.RedirectURIs) > 0 { + if parsed, err := url.Parse(client.RedirectURIs[0]); err == nil { + clientURL = fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) + } + } + record.URL = clientURL + } else { + // Hydra 정보 없음 (삭제됨 등) -> Audit 정보나 ID로 대체 + if record.Name == "" { + record.Name = details.ClientID + } + } + + records[details.ClientID] = record + } + } + } + ordered := make([]*linkedRpRecord, 0, len(records)) for _, record := range records { ordered = append(ordered, record)