forked from baron/baron-sso
RP 로그인 이력 통합
This commit is contained in:
@@ -12,6 +12,7 @@ type OathkeeperAccessLog struct {
|
|||||||
Path string
|
Path string
|
||||||
Status int
|
Status int
|
||||||
LatencyMs int
|
LatencyMs int
|
||||||
|
ClientID string
|
||||||
RP string
|
RP string
|
||||||
Action string
|
Action string
|
||||||
Target string
|
Target string
|
||||||
|
|||||||
@@ -2976,6 +2976,11 @@ type consentClientInfo struct {
|
|||||||
ConsentAt time.Time
|
ConsentAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type loginClientInfo struct {
|
||||||
|
ClientID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||||
if h.AuditRepo == nil && h.OathkeeperRepo == nil {
|
if h.AuditRepo == nil && h.OathkeeperRepo == nil {
|
||||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Audit service unavailable"})
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Audit service unavailable"})
|
||||||
@@ -3123,7 +3128,8 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|||||||
for batch := 0; batch < maxBatches && len(oathkeeperLogs) < fetchLimit; batch++ {
|
for batch := 0; batch < maxBatches && len(oathkeeperLogs) < fetchLimit; batch++ {
|
||||||
logs, err := h.OathkeeperRepo.FindPageBySubject(c.Context(), subject, fetchLimit, currentCursor)
|
logs, err := h.OathkeeperRepo.FindPageBySubject(c.Context(), subject, fetchLimit, currentCursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve oathkeeper logs"})
|
slog.Warn("Failed to retrieve oathkeeper logs", "error", err)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if len(logs) == 0 {
|
if len(logs) == 0 {
|
||||||
break
|
break
|
||||||
@@ -3159,9 +3165,53 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginChallengeCache := make(map[string]loginClientInfo)
|
||||||
|
resolveLoginClient := func(challenge string) (loginClientInfo, bool) {
|
||||||
|
challenge = strings.TrimSpace(challenge)
|
||||||
|
if challenge == "" || h.Hydra == nil {
|
||||||
|
return loginClientInfo{}, false
|
||||||
|
}
|
||||||
|
if cached, ok := loginChallengeCache[challenge]; ok {
|
||||||
|
return cached, cached.ClientID != ""
|
||||||
|
}
|
||||||
|
loginReq, err := h.Hydra.GetLoginRequest(c.Context(), challenge)
|
||||||
|
if err != nil || loginReq == nil {
|
||||||
|
loginChallengeCache[challenge] = loginClientInfo{}
|
||||||
|
return loginClientInfo{}, false
|
||||||
|
}
|
||||||
|
clientID := strings.TrimSpace(loginReq.Client.ClientID)
|
||||||
|
if clientID == "" {
|
||||||
|
loginChallengeCache[challenge] = loginClientInfo{}
|
||||||
|
return loginClientInfo{}, false
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(loginReq.Client.ClientName)
|
||||||
|
if name == "" {
|
||||||
|
name = clientID
|
||||||
|
}
|
||||||
|
info := loginClientInfo{
|
||||||
|
ClientID: clientID,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
loginChallengeCache[challenge] = info
|
||||||
|
return info, true
|
||||||
|
}
|
||||||
|
|
||||||
items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs))
|
items := make([]authTimelineItem, 0, len(authLogs)+len(oathkeeperLogs))
|
||||||
for i := range authLogs {
|
for i := range authLogs {
|
||||||
log := authLogs[i]
|
log := authLogs[i]
|
||||||
|
appName := "Baron 통합로그인"
|
||||||
|
clientID := ""
|
||||||
|
path := strings.ToLower(extractAuditPath(log))
|
||||||
|
if strings.Contains(path, "/api/v1/auth/oidc/login/accept") {
|
||||||
|
appName = "OIDC 로그인"
|
||||||
|
loginChallenge := extractLoginChallengeFromAuditDetails(log.Details)
|
||||||
|
if loginChallenge != "" {
|
||||||
|
if info, ok := resolveLoginClient(loginChallenge); ok {
|
||||||
|
appName = info.Name
|
||||||
|
clientID = info.ClientID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
item := authTimelineItem{
|
item := authTimelineItem{
|
||||||
EventID: log.EventID,
|
EventID: log.EventID,
|
||||||
Timestamp: log.Timestamp,
|
Timestamp: log.Timestamp,
|
||||||
@@ -3174,7 +3224,8 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|||||||
UserAgent: log.UserAgent,
|
UserAgent: log.UserAgent,
|
||||||
Details: log.Details,
|
Details: log.Details,
|
||||||
Source: "backend",
|
Source: "backend",
|
||||||
AppName: "Baron 통합로그인",
|
AppName: appName,
|
||||||
|
ClientID: clientID,
|
||||||
}
|
}
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
@@ -3432,6 +3483,10 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
|||||||
if err != nil || identity == nil {
|
if err != nil || identity == nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load identity")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load identity")
|
||||||
}
|
}
|
||||||
|
c.Locals("user_id", consentRequest.Subject)
|
||||||
|
if loginID := pickLoginIDFromTraits(identity.Traits); loginID != "" {
|
||||||
|
c.Locals("login_id", loginID)
|
||||||
|
}
|
||||||
sessionClaims := buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope)
|
sessionClaims := buildOidcClaimsFromTraits(identity.Traits, consentRequest.RequestedScope)
|
||||||
|
|
||||||
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest, sessionClaims)
|
acceptResp, err := h.Hydra.AcceptConsentRequest(c.Context(), req.ConsentChallenge, consentRequest, sessionClaims)
|
||||||
@@ -3458,6 +3513,21 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
|||||||
if err != nil || subject == "" {
|
if err != nil || subject == "" {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Authentication required")
|
return fiber.NewError(fiber.StatusUnauthorized, "Authentication required")
|
||||||
}
|
}
|
||||||
|
c.Locals("user_id", subject)
|
||||||
|
if sessionID, ok := c.Locals("session_id").(string); ok && sessionID != "" {
|
||||||
|
c.Locals("approved_session_id", sessionID)
|
||||||
|
} else if token := h.getBearerToken(c); token != "" {
|
||||||
|
if derivedID := extractSessionIDFromJWT(token); derivedID != "" {
|
||||||
|
c.Locals("approved_session_id", derivedID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if h.KratosAdmin != nil {
|
||||||
|
if identity, err := h.KratosAdmin.GetIdentity(c.Context(), subject); err == nil && identity != nil {
|
||||||
|
if loginID := pickLoginIDFromTraits(identity.Traits); loginID != "" {
|
||||||
|
c.Locals("login_id", loginID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, subject)
|
acceptResp, err := h.Hydra.AcceptLoginRequest(c.Context(), req.LoginChallenge, subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3715,6 +3785,9 @@ func shouldSkipAuthTimeline(log domain.AuditLog) bool {
|
|||||||
if path != "" && strings.Contains(path, "/api/v1/auth/enchanted-link/init") {
|
if path != "" && strings.Contains(path, "/api/v1/auth/enchanted-link/init") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if path != "" && strings.Contains(path, "/api/v1/auth/consent/accept") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if path != "" && (strings.Contains(path, "/api/v1/auth/magic-link/verify") ||
|
if path != "" && (strings.Contains(path, "/api/v1/auth/magic-link/verify") ||
|
||||||
strings.Contains(path, "/api/v1/auth/login/code/verify")) {
|
strings.Contains(path, "/api/v1/auth/login/code/verify")) {
|
||||||
sessionID := log.SessionID
|
sessionID := log.SessionID
|
||||||
@@ -3922,6 +3995,8 @@ func deriveAuthMethod(log domain.AuditLog) string {
|
|||||||
return "QR"
|
return "QR"
|
||||||
case strings.Contains(path, "/api/v1/auth/qr/poll"):
|
case strings.Contains(path, "/api/v1/auth/qr/poll"):
|
||||||
return "QR"
|
return "QR"
|
||||||
|
case strings.Contains(path, "/api/v1/auth/oidc/login/accept"):
|
||||||
|
return "OIDC 로그인"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -4001,7 +4076,37 @@ func extractLoginIDFromAuditDetails(details string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractLoginChallengeFromAuditDetails(details string) string {
|
||||||
|
if details == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
payload, err := parseAuditDetails(details)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if raw, ok := payload["login_challenge"].(string); ok && raw != "" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
if raw, ok := payload["loginChallenge"].(string); ok && raw != "" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
body := extractRequestBody(payload)
|
||||||
|
if body == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if raw, ok := body["login_challenge"].(string); ok && raw != "" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
if raw, ok := body["loginChallenge"].(string); ok && raw != "" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func extractClientIDFromOathkeeperLog(log domain.OathkeeperAccessLog) string {
|
func extractClientIDFromOathkeeperLog(log domain.OathkeeperAccessLog) string {
|
||||||
|
if value := strings.TrimSpace(log.ClientID); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
if value := strings.TrimSpace(log.RP); value != "" {
|
if value := strings.TrimSpace(log.RP); value != "" {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ func AuditMiddleware(config AuditConfig) fiber.Handler {
|
|||||||
if sessionID != "" {
|
if sessionID != "" {
|
||||||
details["session_id"] = sessionID
|
details["session_id"] = sessionID
|
||||||
}
|
}
|
||||||
|
if approvedSessionID, ok := c.Locals("approved_session_id").(string); ok && approvedSessionID != "" {
|
||||||
|
details["approved_session_id"] = approvedSessionID
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
details["error"] = err.Error()
|
details["error"] = err.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"baron-sso-backend/internal/domain"
|
"baron-sso-backend/internal/domain"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
"github.com/ClickHouse/clickhouse-go/v2"
|
||||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||||
@@ -36,10 +37,77 @@ func (r *OathkeeperClickHouseRepository) FindPageBySubject(ctx context.Context,
|
|||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 50
|
limit = 50
|
||||||
}
|
}
|
||||||
query := `
|
query, args := buildOathkeeperQuery(subject, limit, cursor, true)
|
||||||
SELECT timestamp, request_id, method, path, status, latency_ms, rp, action, target, subject, client_ip, user_agent, decision, trace_id, span_id, raw
|
rows, err := r.conn.Query(ctx, query, args...)
|
||||||
FROM oathkeeper_access_logs
|
if err != nil && isMissingColumnError(err, "client_id") {
|
||||||
`
|
query, args = buildOathkeeperQuery(subject, limit, cursor, false)
|
||||||
|
rows, err = r.conn.Query(ctx, query, args...)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query oathkeeper logs: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
withClientID := strings.Contains(query, "client_id")
|
||||||
|
var logs []domain.OathkeeperAccessLog
|
||||||
|
for rows.Next() {
|
||||||
|
var log domain.OathkeeperAccessLog
|
||||||
|
if withClientID {
|
||||||
|
if err := rows.Scan(
|
||||||
|
&log.Timestamp,
|
||||||
|
&log.RequestID,
|
||||||
|
&log.Method,
|
||||||
|
&log.Path,
|
||||||
|
&log.Status,
|
||||||
|
&log.LatencyMs,
|
||||||
|
&log.ClientID,
|
||||||
|
&log.RP,
|
||||||
|
&log.Action,
|
||||||
|
&log.Target,
|
||||||
|
&log.Subject,
|
||||||
|
&log.ClientIP,
|
||||||
|
&log.UserAgent,
|
||||||
|
&log.Decision,
|
||||||
|
&log.TraceID,
|
||||||
|
&log.SpanID,
|
||||||
|
&log.Raw,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan oathkeeper log: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := rows.Scan(
|
||||||
|
&log.Timestamp,
|
||||||
|
&log.RequestID,
|
||||||
|
&log.Method,
|
||||||
|
&log.Path,
|
||||||
|
&log.Status,
|
||||||
|
&log.LatencyMs,
|
||||||
|
&log.RP,
|
||||||
|
&log.Action,
|
||||||
|
&log.Target,
|
||||||
|
&log.Subject,
|
||||||
|
&log.ClientIP,
|
||||||
|
&log.UserAgent,
|
||||||
|
&log.Decision,
|
||||||
|
&log.TraceID,
|
||||||
|
&log.SpanID,
|
||||||
|
&log.Raw,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan oathkeeper log: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logs = append(logs, log)
|
||||||
|
}
|
||||||
|
return logs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOathkeeperQuery(subject string, limit int, cursor *domain.AuditCursor, withClientID bool) (string, []any) {
|
||||||
|
selectCols := "timestamp, request_id, method, path, status, latency_ms, rp, action, target, subject, client_ip, user_agent, decision, trace_id, span_id, raw"
|
||||||
|
if withClientID {
|
||||||
|
selectCols = "timestamp, request_id, method, path, status, latency_ms, client_id, rp, action, target, subject, client_ip, user_agent, decision, trace_id, span_id, raw"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf("SELECT %s FROM oathkeeper_access_logs", selectCols)
|
||||||
args := make([]any, 0, 5)
|
args := make([]any, 0, 5)
|
||||||
if subject != "" {
|
if subject != "" {
|
||||||
query += `
|
query += `
|
||||||
@@ -63,39 +131,25 @@ func (r *OathkeeperClickHouseRepository) FindPageBySubject(ctx context.Context,
|
|||||||
LIMIT ?
|
LIMIT ?
|
||||||
`
|
`
|
||||||
args = append(args, limit)
|
args = append(args, limit)
|
||||||
|
return query, args
|
||||||
|
}
|
||||||
|
|
||||||
rows, err := r.conn.Query(ctx, query, args...)
|
func isMissingColumnError(err error, column string) bool {
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return nil, fmt.Errorf("failed to query oathkeeper logs: %w", err)
|
return false
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
msg := strings.ToLower(err.Error())
|
||||||
|
column = strings.ToLower(column)
|
||||||
var logs []domain.OathkeeperAccessLog
|
if strings.Contains(msg, "unknown identifier") && strings.Contains(msg, column) {
|
||||||
for rows.Next() {
|
return true
|
||||||
var log domain.OathkeeperAccessLog
|
|
||||||
if err := rows.Scan(
|
|
||||||
&log.Timestamp,
|
|
||||||
&log.RequestID,
|
|
||||||
&log.Method,
|
|
||||||
&log.Path,
|
|
||||||
&log.Status,
|
|
||||||
&log.LatencyMs,
|
|
||||||
&log.RP,
|
|
||||||
&log.Action,
|
|
||||||
&log.Target,
|
|
||||||
&log.Subject,
|
|
||||||
&log.ClientIP,
|
|
||||||
&log.UserAgent,
|
|
||||||
&log.Decision,
|
|
||||||
&log.TraceID,
|
|
||||||
&log.SpanID,
|
|
||||||
&log.Raw,
|
|
||||||
); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to scan oathkeeper log: %w", err)
|
|
||||||
}
|
|
||||||
logs = append(logs, log)
|
|
||||||
}
|
}
|
||||||
return logs, nil
|
if strings.Contains(msg, "unknown expression identifier") && strings.Contains(msg, column) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(msg, "missing columns") && strings.Contains(msg, column) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *OathkeeperClickHouseRepository) Ping(ctx context.Context) error {
|
func (r *OathkeeperClickHouseRepository) Ping(ctx context.Context) error {
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ type HydraConsentRequest struct {
|
|||||||
Client HydraClient `json:"client"`
|
Client HydraClient `json:"client"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HydraLoginRequest struct {
|
||||||
|
Challenge string `json:"challenge"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Skip bool `json:"skip"`
|
||||||
|
Client HydraClient `json:"client"`
|
||||||
|
}
|
||||||
|
|
||||||
type HydraConsentSession struct {
|
type HydraConsentSession struct {
|
||||||
ConsentRequestID string `json:"consent_request_id,omitempty"`
|
ConsentRequestID string `json:"consent_request_id,omitempty"`
|
||||||
Subject string `json:"subject,omitempty"`
|
Subject string `json:"subject,omitempty"`
|
||||||
@@ -402,6 +409,39 @@ func (s *HydraAdminService) GetConsentRequest(ctx context.Context, challenge str
|
|||||||
return &consentReq, nil
|
return &consentReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *HydraAdminService) GetLoginRequest(ctx context.Context, challenge string) (*HydraLoginRequest, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"login_challenge": challenge,
|
||||||
|
}
|
||||||
|
endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/login", params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hydra admin: create request for get login failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.httpClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hydra admin: get login request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("hydra admin: get login failed status=%d body=%s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginReq HydraLoginRequest
|
||||||
|
if err := json.Unmarshal(body, &loginReq); err != nil {
|
||||||
|
return nil, fmt.Errorf("hydra admin: decode get login response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &loginReq, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *HydraAdminService) AcceptConsentRequest(ctx context.Context, challenge string, grantInfo *HydraConsentRequest, sessionClaims map[string]any) (*AcceptConsentRequestResponse, error) {
|
func (s *HydraAdminService) AcceptConsentRequest(ctx context.Context, challenge string, grantInfo *HydraConsentRequest, sessionClaims map[string]any) (*AcceptConsentRequestResponse, error) {
|
||||||
params := map[string]string{
|
params := map[string]string{
|
||||||
"consent_challenge": challenge,
|
"consent_challenge": challenge,
|
||||||
|
|||||||
@@ -449,12 +449,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAuthMethodCell(AuditLogEntry log, String authMethod) {
|
Widget _buildAuthMethodCell(AuditLogEntry log, String authMethod) {
|
||||||
if (authMethod != 'QR') {
|
final isOidc = authMethod.contains('OIDC');
|
||||||
|
if (authMethod != 'QR' && !isOidc) {
|
||||||
final approvedUserAgent = log.detailMap['approved_user_agent']?.toString() ?? '';
|
final approvedUserAgent = log.detailMap['approved_user_agent']?.toString() ?? '';
|
||||||
final approvedIp = log.detailMap['approved_ip']?.toString() ?? '';
|
final approvedIp = log.detailMap['approved_ip']?.toString() ?? '';
|
||||||
final hasApproverMeta = approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
|
final hasApproverMeta = approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
|
||||||
if (!authMethod.startsWith('링크') || !hasApproverMeta) {
|
if (!authMethod.startsWith('링크') || !hasApproverMeta) {
|
||||||
return _selectableText(authMethod);
|
return _selectableText(authMethod);
|
||||||
}
|
}
|
||||||
final deviceLabel = _deviceLabelFromUserAgent(approvedUserAgent);
|
final deviceLabel = _deviceLabelFromUserAgent(approvedUserAgent);
|
||||||
final tooltip = [
|
final tooltip = [
|
||||||
@@ -472,10 +473,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final approvedSessionId = log.detailMap['approved_session_id']?.toString() ?? '';
|
final approvedSessionId = (log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ?? false)
|
||||||
|
? log.detailMap['approved_session_id'].toString()
|
||||||
|
: log.sessionId;
|
||||||
|
final tooltipLabel = isOidc ? '승인한 Userfront 세션 ID' : '승인한 세션 ID';
|
||||||
final tooltip = approvedSessionId.isEmpty
|
final tooltip = approvedSessionId.isEmpty
|
||||||
? '승인한 세션 ID 없음'
|
? '$tooltipLabel 없음'
|
||||||
: '승인한 세션 ID: $approvedSessionId\n클릭하면 복사됩니다.';
|
: '$tooltipLabel: $approvedSessionId\n클릭하면 복사됩니다.';
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: approvedSessionId.isEmpty
|
onTap: approvedSessionId.isEmpty
|
||||||
? null
|
? null
|
||||||
@@ -490,7 +494,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: tooltip,
|
message: tooltip,
|
||||||
child: Text(
|
child: Text(
|
||||||
'QR',
|
isOidc ? authMethod : 'QR',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: approvedSessionId.isEmpty ? _ink : Colors.blueAccent,
|
color: approvedSessionId.isEmpty ? _ink : Colors.blueAccent,
|
||||||
decoration:
|
decoration:
|
||||||
@@ -502,12 +506,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAuthMethodLine(AuditLogEntry log, String authMethod) {
|
Widget _buildAuthMethodLine(AuditLogEntry log, String authMethod) {
|
||||||
if (authMethod != 'QR') {
|
final isOidc = authMethod.contains('OIDC');
|
||||||
|
if (authMethod != 'QR' && !isOidc) {
|
||||||
final approvedUserAgent = log.detailMap['approved_user_agent']?.toString() ?? '';
|
final approvedUserAgent = log.detailMap['approved_user_agent']?.toString() ?? '';
|
||||||
final approvedIp = log.detailMap['approved_ip']?.toString() ?? '';
|
final approvedIp = log.detailMap['approved_ip']?.toString() ?? '';
|
||||||
final hasApproverMeta = approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
|
final hasApproverMeta = approvedUserAgent.isNotEmpty || approvedIp.isNotEmpty;
|
||||||
if (!authMethod.startsWith('링크') || !hasApproverMeta) {
|
if (!authMethod.startsWith('링크') || !hasApproverMeta) {
|
||||||
return _selectableText('인증수단: $authMethod');
|
return _selectableText('인증수단: $authMethod');
|
||||||
}
|
}
|
||||||
final deviceLabel = _deviceLabelFromUserAgent(approvedUserAgent);
|
final deviceLabel = _deviceLabelFromUserAgent(approvedUserAgent);
|
||||||
final tooltip = [
|
final tooltip = [
|
||||||
@@ -525,7 +530,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final approvedSessionId = log.detailMap['approved_session_id']?.toString() ?? '';
|
final approvedSessionId = (log.detailMap['approved_session_id']?.toString().trim().isNotEmpty ?? false)
|
||||||
|
? log.detailMap['approved_session_id'].toString()
|
||||||
|
: log.sessionId;
|
||||||
|
final tooltipLabel = isOidc ? '승인한 Userfront 세션 ID' : '승인한 세션 ID';
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: approvedSessionId.isEmpty
|
onTap: approvedSessionId.isEmpty
|
||||||
? null
|
? null
|
||||||
@@ -539,10 +547,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
},
|
},
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: approvedSessionId.isEmpty
|
message: approvedSessionId.isEmpty
|
||||||
? '승인한 세션 ID 없음'
|
? '$tooltipLabel 없음'
|
||||||
: '승인한 세션 ID: $approvedSessionId\n탭하면 복사됩니다.',
|
: '$tooltipLabel: $approvedSessionId\n탭하면 복사됩니다.',
|
||||||
child: Text(
|
child: Text(
|
||||||
'인증수단: QR',
|
'인증수단: ${isOidc ? authMethod : 'QR'}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: approvedSessionId.isEmpty ? _ink : Colors.blueAccent,
|
color: approvedSessionId.isEmpty ? _ink : Colors.blueAccent,
|
||||||
decoration: approvedSessionId.isEmpty
|
decoration: approvedSessionId.isEmpty
|
||||||
|
|||||||
Reference in New Issue
Block a user