1
0
forked from baron/baron-sso

접근 이력 스크롤 조회 기능 추가

This commit is contained in:
Lectom C Han
2026-02-02 14:03:54 +09:00
parent 7e662c9878
commit 1c0a5ed272
15 changed files with 1265 additions and 231 deletions

View File

@@ -38,6 +38,12 @@ const (
prefixLoginCodeSmsTarget = "login_code_sms_target:"
prefixLoginCodeSmsLookup = "login_code_sms_lookup:"
prefixLoginCodeShort = "login_code_short:"
prefixLoginCodeValue = "login_code_value:"
prefixLoginIDRaw = "login_id_raw:"
prefixLoginMethod = "login_method:"
prefixLoginFlow = "login_flow:"
prefixLoginStrategy = "login_strategy:"
prefixLoginApproverMeta = "login_approver_meta:"
prefixLoginCodeSmsOnly = "login_code_sms_only:"
prefixLoginCodeQrPending = "login_code_qr_pending:"
prefixLoginCodeQr = "login_code_qr:"
@@ -53,6 +59,10 @@ const (
statusPending = "pending"
statusSuccess = "success"
// Login Flows
loginFlowCode = "code"
loginFlowLink = "link"
// Durations
defaultExpiration = 5 * time.Minute
signupStateExpiration = 10 * time.Minute
@@ -635,7 +645,16 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
_ = h.RedisService.Set(prefixLoginCode+keyLoginID, init.FlowID, loginCodeExpiration)
}
pendingRef := GenerateSecureToken(3)
h.RedisService.Set(prefixSession+pendingRef, fmt.Sprintf(`{"status":"%s"}`, statusPending), loginCodeExpiration)
sessionData, _ := json.Marshal(map[string]string{
"status": statusPending,
"loginId": keyLoginID,
})
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), loginCodeExpiration)
intent := loginFlowLink
if req.CodeOnly {
intent = loginFlowCode
}
h.storeLoginMeta(pendingRef, loginID, req.Method, intent, loginFlowCode, loginCodeExpiration)
_ = h.RedisService.Set(prefixLoginCodePending+keyLoginID, pendingRef, loginCodeExpiration)
if drySend {
_ = h.RedisService.Set(prefixDrySend+keyLoginID, pendingRef, loginCodeExpiration)
@@ -677,11 +696,20 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error {
slog.Info("[Enchanted] Initiating enchanted link", "loginID", loginID, "token", token, "pendingRef", pendingRef)
// Store in Redis
h.RedisService.Set(prefixSession+pendingRef, fmt.Sprintf(`{"status":"%s"}`, statusPending), defaultExpiration)
sessionData, _ := json.Marshal(map[string]string{
"status": statusPending,
"loginId": lookupLoginID,
})
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), defaultExpiration)
h.RedisService.Set(prefixToken+token, fmt.Sprintf(`{"pendingRef":"%s","loginId":"%s"}`, pendingRef, lookupLoginID), defaultExpiration)
if drySend {
_ = h.RedisService.Set(prefixDrySend+lookupLoginID, pendingRef, defaultExpiration)
}
intent := loginFlowLink
if req.CodeOnly {
intent = loginFlowCode
}
h.storeLoginMeta(pendingRef, loginID, req.Method, intent, loginFlowLink, defaultExpiration)
// Generate Link
slog.Info("[Enchanted] Read USERFRONT_URL", "url", userfrontURL)
@@ -787,13 +815,41 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"})
}
authInfo, err := h.IdpProvider.IssueSession(loginID)
if err != nil {
if errors.Is(err, domain.ErrNotSupported) {
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"})
loginStrategy := h.loadLoginStrategy(req.PendingRef)
if loginStrategy == "" {
loginStrategy = loginFlowLink
}
var authInfo *domain.AuthInfo
var err error
if loginStrategy == loginFlowCode {
code, _ := h.RedisService.Get(prefixLoginCodeValue + req.PendingRef)
code = normalizeLoginCode(code)
if code == "" {
slog.Warn("[Poll] Missing login code for approved flow", "pendingRef", req.PendingRef)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login code expired"})
}
flowID, _ := h.RedisService.Get(prefixLoginCode + loginID)
if flowID == "" {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Login flow expired"})
}
authInfo, err = h.IdpProvider.VerifyLoginCode(loginID, flowID, code)
if err != nil {
if errors.Is(err, domain.ErrNotSupported) {
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"})
}
slog.Error("[Poll] IDP code verify failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to verify login code"})
}
} else {
authInfo, err = h.IdpProvider.IssueSession(loginID)
if err != nil {
if errors.Is(err, domain.ErrNotSupported) {
return c.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"error": "Login method not supported"})
}
slog.Error("[Poll] IDP session issue failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
}
slog.Error("[Poll] IDP session issue failed", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
}
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue session"})
@@ -802,6 +858,13 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error {
c.Locals("login_id", loginID)
setSessionIDLocal(c, authInfo.SessionToken)
sessionID := extractSessionIDFromToken(authInfo.SessionToken)
if sessionID == "" && authInfo.SessionToken != nil && authInfo.SessionToken.JWT != "" {
if resolved, err := h.getKratosSessionID(authInfo.SessionToken.JWT); err == nil && resolved != "" {
sessionID = resolved
authInfo.SessionToken.SessionID = resolved
setSessionIDLocal(c, authInfo.SessionToken)
}
}
sessionData := map[string]string{
"status": statusSuccess,
@@ -814,6 +877,14 @@ func (h *AuthHandler) PollEnchantedLink(c *fiber.Ctx) error {
h.RedisService.Set(prefixSession+req.PendingRef, string(sessionDataJSON), defaultExpiration)
h.writeLinkAuditLog(loginID, req.PendingRef, authInfo.SessionToken, c)
h.clearLoginMeta(req.PendingRef)
if loginStrategy == loginFlowCode {
h.RedisService.Delete(prefixLoginCode + loginID)
h.RedisService.Delete(prefixLoginCodePending + loginID)
h.RedisService.Delete(prefixLoginCodeSmsTarget + loginID)
h.RedisService.Delete(prefixLoginCodeSmsLookup + loginID)
h.RedisService.Delete(prefixLoginCodeValue + req.PendingRef)
}
return c.JSON(fiber.Map{
"sessionJwt": authInfo.SessionToken.JWT,
@@ -852,11 +923,14 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error {
slog.Info("[Verify] Token valid", "loginID", loginID, "pendingRef", pendingRef)
if req.VerifyOnly {
c.Locals("auth_timeline_skip", true)
if pendingRef == "" || loginID == "" {
slog.Warn("[Verify] Missing pendingRef/loginID for verify-only", "token", req.Token)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"})
}
h.storeLoginApproverMeta(pendingRef, c, defaultExpiration)
// 승인 전용: 세션 발급 없이 승인 상태만 기록
sessionData, _ := json.Marshal(map[string]string{
"status": "approved",
@@ -919,6 +993,7 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
LoginID string `json:"loginId"`
Code string `json:"code"`
PendingRef string `json:"pendingRef"`
VerifyOnly bool `json:"verifyOnly,omitempty"`
}
if err := c.BodyParser(&req); err != nil {
slog.Error("[LoginCode] Body parse error", "error", err)
@@ -945,6 +1020,43 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Login flow expired"})
}
if req.VerifyOnly {
c.Locals("auth_timeline_skip", true)
effectiveLoginID := lookupLoginID
if !strings.Contains(loginID, "@") {
if mapped, _ := h.RedisService.Get(prefixLoginCodeSmsLookup + lookupLoginID); mapped != "" {
effectiveLoginID = mapped
}
}
pendingRef := strings.TrimSpace(req.PendingRef)
storedRef, _ := h.RedisService.Get(prefixLoginCodePending + lookupLoginID)
if pendingRef == "" {
pendingRef = storedRef
} else if storedRef != "" && pendingRef != storedRef {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"})
}
if pendingRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"})
}
expectedCode, _ := h.RedisService.Get(prefixLoginCodeValue + pendingRef)
expectedCode = normalizeLoginCode(expectedCode)
inputCode := normalizeLoginCode(req.Code)
if expectedCode == "" || inputCode == "" || inputCode != expectedCode {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid code"})
}
h.storeLoginApproverMeta(pendingRef, c, loginCodeExpiration)
sessionData, _ := json.Marshal(map[string]string{
"status": "approved",
"loginId": effectiveLoginID,
})
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), loginCodeExpiration)
return c.JSON(fiber.Map{
"status": "approved",
"pendingRef": pendingRef,
"message": "Login approved",
})
}
authInfo, err := h.IdpProvider.VerifyLoginCode(lookupLoginID, flowID, req.Code)
if err != nil {
if errors.Is(err, domain.ErrNotSupported) {
@@ -997,6 +1109,7 @@ func (h *AuthHandler) VerifyLoginCode(c *fiber.Ctx) error {
func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
var req struct {
ShortCode string `json:"shortCode"`
VerifyOnly bool `json:"verifyOnly,omitempty"`
}
if err := c.BodyParser(&req); err != nil {
slog.Error("[LoginShortCode] Body parse error", "error", err)
@@ -1021,6 +1134,29 @@ func (h *AuthHandler) VerifyLoginShortCode(c *fiber.Ctx) error {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"})
}
if req.VerifyOnly {
c.Locals("auth_timeline_skip", true)
if payload.PendingRef == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid session reference"})
}
normalizedCode := normalizeLoginCode(payload.Code)
if normalizedCode != "" {
h.RedisService.Set(prefixLoginCodeValue+payload.PendingRef, normalizedCode, loginCodeExpiration)
}
h.storeLoginApproverMeta(payload.PendingRef, c, loginCodeExpiration)
sessionData, _ := json.Marshal(map[string]string{
"status": "approved",
"loginId": payload.LoginID,
})
h.RedisService.Set(prefixSession+payload.PendingRef, string(sessionData), loginCodeExpiration)
h.RedisService.Delete(prefixLoginCodeShort + shortCode)
return c.JSON(fiber.Map{
"status": "approved",
"pendingRef": payload.PendingRef,
"message": "Login approved",
})
}
if h.IdpProvider == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Identity provider unavailable"})
}
@@ -1825,6 +1961,20 @@ func (h *AuthHandler) buildKratosCourierMessage(req *kratosCourierRequest) (stri
}
}
if loginCode != "" && label == "로그인" {
loginID := req.Recipient
if !strings.Contains(loginID, "@") {
loginID = normalizePhoneForLoginID(loginID)
}
pendingRef, _ := h.RedisService.Get(prefixLoginCodePending + loginID)
if pendingRef != "" {
normalizedCode := normalizeLoginCode(loginCode)
if normalizedCode != "" {
_ = h.RedisService.Set(prefixLoginCodeValue+pendingRef, normalizedCode, loginCodeExpiration)
}
}
}
if code == "" {
return subject, fmt.Sprintf("[Baron 통합로그인] %s 요청이 도착했습니다", label)
}
@@ -2241,6 +2391,39 @@ func (h *AuthHandler) loadQrMeta(pendingRef string) (qrMeta, bool) {
return meta, true
}
func (h *AuthHandler) storeLoginApproverMeta(pendingRef string, c *fiber.Ctx, ttl time.Duration) {
if h.RedisService == nil || pendingRef == "" || c == nil {
return
}
meta := qrMeta{
IPAddress: extractClientIPFromHeaders(c),
UserAgent: c.Get("User-Agent"),
}
raw, err := json.Marshal(meta)
if err != nil {
return
}
_ = h.RedisService.Set(prefixLoginApproverMeta+pendingRef, string(raw), ttl)
}
func (h *AuthHandler) loadLoginApproverMeta(pendingRef string) (qrMeta, bool) {
if h.RedisService == nil || pendingRef == "" {
return qrMeta{}, false
}
val, err := h.RedisService.Get(prefixLoginApproverMeta + pendingRef)
if err != nil || val == "" {
return qrMeta{}, false
}
var meta qrMeta
if err := json.Unmarshal([]byte(val), &meta); err != nil {
return qrMeta{}, false
}
if meta.IPAddress == "" && meta.UserAgent == "" {
return qrMeta{}, false
}
return meta, true
}
func (h *AuthHandler) storeQrApproverSessionID(pendingRef, sessionID string) {
if h.RedisService == nil || pendingRef == "" || sessionID == "" {
return
@@ -2259,6 +2442,57 @@ func (h *AuthHandler) loadQrApproverSessionID(pendingRef string) string {
return strings.TrimSpace(val)
}
func (h *AuthHandler) storeLoginMeta(pendingRef, loginID, rawMethod, flow, strategy string, ttl time.Duration) {
if h.RedisService == nil || pendingRef == "" {
return
}
method := resolveLoginMethod(rawMethod, loginID)
if method != "" {
_ = h.RedisService.Set(prefixLoginMethod+pendingRef, method, ttl)
}
if flow != "" {
_ = h.RedisService.Set(prefixLoginFlow+pendingRef, flow, ttl)
}
if strategy != "" {
_ = h.RedisService.Set(prefixLoginStrategy+pendingRef, strategy, ttl)
}
if strings.TrimSpace(loginID) != "" {
_ = h.RedisService.Set(prefixLoginIDRaw+pendingRef, loginID, ttl)
}
}
func (h *AuthHandler) loadLoginMeta(pendingRef string) (string, string, string, string) {
if h.RedisService == nil || pendingRef == "" {
return "", "", "", ""
}
method, _ := h.RedisService.Get(prefixLoginMethod + pendingRef)
flow, _ := h.RedisService.Get(prefixLoginFlow + pendingRef)
strategy, _ := h.RedisService.Get(prefixLoginStrategy + pendingRef)
rawLoginID, _ := h.RedisService.Get(prefixLoginIDRaw + pendingRef)
return strings.TrimSpace(method), strings.TrimSpace(flow), strings.TrimSpace(strategy), strings.TrimSpace(rawLoginID)
}
func (h *AuthHandler) loadLoginFlow(pendingRef string) string {
_, flow, _, _ := h.loadLoginMeta(pendingRef)
return flow
}
func (h *AuthHandler) loadLoginStrategy(pendingRef string) string {
_, _, strategy, _ := h.loadLoginMeta(pendingRef)
return strategy
}
func (h *AuthHandler) clearLoginMeta(pendingRef string) {
if h.RedisService == nil || pendingRef == "" {
return
}
_ = h.RedisService.Delete(prefixLoginMethod + pendingRef)
_ = h.RedisService.Delete(prefixLoginFlow + pendingRef)
_ = h.RedisService.Delete(prefixLoginStrategy + pendingRef)
_ = h.RedisService.Delete(prefixLoginIDRaw + pendingRef)
_ = h.RedisService.Delete(prefixLoginApproverMeta + pendingRef)
}
func (h *AuthHandler) writeQrAuditLog(loginID, pendingRef string, sessionToken *domain.Token, approvedSessionID string) {
if h.AuditRepo == nil || pendingRef == "" {
return
@@ -2314,14 +2548,53 @@ func (h *AuthHandler) writeLinkAuditLog(loginID, pendingRef string, sessionToken
meta.UserAgent = c.Get("User-Agent")
}
sessionID := extractSessionIDFromToken(sessionToken)
loginMethod, loginFlow, loginStrategy, rawLoginID := h.loadLoginMeta(pendingRef)
path := "/api/v1/auth/magic-link/verify"
authLabel := "링크"
if loginStrategy == loginFlowCode {
path = "/api/v1/auth/login/code/verify"
}
displayFlow := loginFlow
if displayFlow == "" {
displayFlow = loginStrategy
}
if displayFlow == loginFlowCode {
authLabel = "코드"
} else if displayFlow == loginFlowLink {
authLabel = "링크"
}
logLoginID := loginID
if rawLoginID != "" {
logLoginID = rawLoginID
}
details := map[string]any{
"path": "/api/v1/auth/magic-link/verify",
"login_id": loginID,
"path": path,
"login_id": logLoginID,
"pending_ref": pendingRef,
}
if sessionID != "" {
details["session_id"] = sessionID
}
if loginMethod != "" {
details["login_method"] = loginMethod
}
if loginFlow != "" {
details["login_flow"] = loginFlow
}
if loginStrategy != "" {
details["login_strategy"] = loginStrategy
}
if rawLoginID != "" && rawLoginID != loginID {
details["login_id_effective"] = loginID
}
if approverMeta, ok := h.loadLoginApproverMeta(pendingRef); ok {
if approverMeta.IPAddress != "" {
details["approved_ip"] = approverMeta.IPAddress
}
if approverMeta.UserAgent != "" {
details["approved_user_agent"] = approverMeta.UserAgent
}
}
detailsJSON, _ := json.Marshal(details)
log := &domain.AuditLog{
@@ -2329,12 +2602,12 @@ func (h *AuthHandler) writeLinkAuditLog(loginID, pendingRef string, sessionToken
Timestamp: time.Now(),
UserID: "",
SessionID: sessionID,
EventType: "POST /api/v1/auth/magic-link/verify",
EventType: fmt.Sprintf("POST %s", path),
Status: "success",
IPAddress: meta.IPAddress,
UserAgent: meta.UserAgent,
Details: string(detailsJSON),
AuthMethod: "링크",
AuthMethod: authLabel,
}
_ = h.AuditRepo.Create(log)
}
@@ -2370,6 +2643,16 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
limit = 100
}
cursorRaw := strings.TrimSpace(c.Query("cursor"))
var cursor *domain.AuditCursor
if cursorRaw != "" {
var err error
cursor, err = parseAuditCursor(cursorRaw)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid cursor"})
}
}
profile, err := h.resolveCurrentProfile(c)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
@@ -2384,38 +2667,76 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
fetchLimit = 500
}
logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, nil)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve audit logs"})
}
items := make([]domain.AuditLog, 0, limit)
for _, log := range logs {
if !isAuthEventType(log.EventType) {
continue
nextCursor := ""
currentCursor := cursor
const maxBatches = 10
for batch := 0; batch < maxBatches && len(items) < limit; batch++ {
logs, err := h.AuditRepo.FindPage(c.Context(), fetchLimit, currentCursor)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to retrieve audit logs"})
}
if !matchesAuthTimelineUser(log, profile, candidates) {
continue
if len(logs) == 0 {
nextCursor = ""
break
}
if log.UserID == "" {
log.UserID = profile.ID
var lastScanned *domain.AuditLog
for i := range logs {
log := logs[i]
lastScanned = &log
if !isAuthEventType(log.EventType) {
continue
}
if !matchesAuthTimelineUser(log, profile, candidates) {
continue
}
if shouldSkipAuthTimeline(log) {
continue
}
if log.UserID == "" {
log.UserID = profile.ID
}
log.AuthMethod = deriveAuthMethod(log)
if log.AuthMethod == "" {
continue
}
if log.SessionID == "" {
log.SessionID = extractSessionIDFromAuditDetails(log.Details)
}
items = append(items, log)
if len(items) >= limit {
nextCursor = encodeAuditCursor(log)
break
}
}
log.AuthMethod = deriveAuthMethod(log)
if log.AuthMethod == "" {
continue
}
if log.SessionID == "" {
log.SessionID = extractSessionIDFromAuditDetails(log.Details)
}
items = append(items, log)
if len(items) >= limit {
break
}
if len(logs) < fetchLimit {
nextCursor = ""
break
}
if lastScanned == nil {
nextCursor = ""
break
}
currentCursor = &domain.AuditCursor{
Timestamp: lastScanned.Timestamp,
EventID: lastScanned.EventID,
}
nextCursor = encodeAuditCursor(*lastScanned)
}
return c.JSON(fiber.Map{
"items": items,
"limit": limit,
"items": items,
"limit": limit,
"cursor": cursorRaw,
"next_cursor": nextCursor,
})
}
@@ -2640,6 +2961,54 @@ func extractRequestBody(details map[string]any) map[string]any {
return body
}
func shouldSkipAuthTimeline(log domain.AuditLog) bool {
details, _ := parseAuditDetails(log.Details)
path := strings.ToLower(extractAuditPath(log))
if path != "" && strings.Contains(path, "/api/v1/auth/enchanted-link/init") {
return true
}
if path != "" && (strings.Contains(path, "/api/v1/auth/magic-link/verify") ||
strings.Contains(path, "/api/v1/auth/login/code/verify")) {
sessionID := log.SessionID
if sessionID == "" {
sessionID = extractSessionIDFromAuditDetails(log.Details)
}
if sessionID == "" {
return true
}
}
if details != nil {
if raw, ok := details["auth_timeline_skip"]; ok {
switch value := raw.(type) {
case bool:
if value {
return true
}
case string:
if strings.EqualFold(strings.TrimSpace(value), "true") || value == "1" {
return true
}
}
}
}
requestBody := extractRequestBody(details)
if requestBody != nil {
if raw, ok := requestBody["verifyOnly"]; ok {
switch value := raw.(type) {
case bool:
if value {
return true
}
case string:
if strings.EqualFold(strings.TrimSpace(value), "true") || value == "1" {
return true
}
}
}
}
return false
}
func loginIDKind(loginID string) string {
normalized := strings.TrimSpace(loginID)
if normalized == "" {
@@ -2651,6 +3020,31 @@ func loginIDKind(loginID string) string {
return "phone"
}
func resolveLoginMethod(rawMethod, loginID string) string {
method := strings.ToLower(strings.TrimSpace(rawMethod))
if method == "sms" || method == "email" {
return method
}
if strings.TrimSpace(loginID) == "" {
return ""
}
if strings.Contains(loginID, "@") {
return "email"
}
return "sms"
}
func loginMethodLabel(method string) string {
switch strings.ToLower(strings.TrimSpace(method)) {
case "sms":
return "SMS"
case "email":
return "Email"
default:
return ""
}
}
func deriveAuthMethod(log domain.AuditLog) string {
path := strings.ToLower(extractAuditPath(log))
if path == "" {
@@ -2661,6 +3055,57 @@ func deriveAuthMethod(log domain.AuditLog) string {
kind := loginIDKind(loginID)
details, _ := parseAuditDetails(log.Details)
requestBody := extractRequestBody(details)
if details != nil {
if raw, ok := details["auth_timeline_skip"]; ok {
switch value := raw.(type) {
case bool:
if value {
return ""
}
case string:
if strings.EqualFold(strings.TrimSpace(value), "true") || value == "1" {
return ""
}
}
}
}
if requestBody != nil {
if raw, ok := requestBody["verifyOnly"]; ok {
switch value := raw.(type) {
case bool:
if value {
return ""
}
case string:
if strings.EqualFold(strings.TrimSpace(value), "true") || value == "1" {
return ""
}
}
}
}
if path != "" && (strings.Contains(path, "/api/v1/auth/qr/init") ||
strings.Contains(path, "/api/v1/auth/qr/poll") ||
strings.Contains(path, "/api/v1/auth/qr/approve")) {
return "QR"
}
if details != nil {
rawFlow, _ := details["login_flow"].(string)
rawMethod, _ := details["login_method"].(string)
flow := strings.ToLower(strings.TrimSpace(rawFlow))
methodLabel := loginMethodLabel(rawMethod)
switch flow {
case loginFlowCode:
if methodLabel != "" {
return fmt.Sprintf("코드(%s)", methodLabel)
}
return "코드"
case loginFlowLink:
if methodLabel != "" {
return fmt.Sprintf("링크(%s)", methodLabel)
}
return "링크"
}
}
switch {
case strings.Contains(path, "/api/v1/auth/password/login"):