forked from baron/baron-sso
접근 이력 스크롤 조회 기능 추가
This commit is contained in:
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user