package store import ( "sort" "strings" "time" "github.com/google/uuid" ) func (m *Memory) AppendAudit(in AuditAppendInput) (*AuditEntry, error) { if strings.TrimSpace(in.TenantID) == "" || strings.TrimSpace(in.Action) == "" || strings.TrimSpace(in.Summary) == "" { return nil, ErrInvalidInput } sev := strings.TrimSpace(in.Severity) if sev == "" { sev = AuditSeverityInfo } if !ValidAuditSeverity(sev) { return nil, ErrInvalidInput } now := time.Now().UTC() row := &AuditEntry{ ID: uuid.NewString(), TenantID: strings.TrimSpace(in.TenantID), EventID: "bgp-" + uuid.NewString(), SourceApp: AuditSourceAppBGP, Action: strings.TrimSpace(in.Action), Severity: sev, ActorUserID: strings.TrimSpace(in.ActorUserID), ActorEmail: strings.TrimSpace(in.ActorEmail), ActorName: strings.TrimSpace(in.ActorName), ActorAPIKeyPrefix: strings.TrimSpace(in.ActorAPIKeyPrefix), TargetType: strings.TrimSpace(in.TargetType), TargetID: strings.TrimSpace(in.TargetID), Summary: strings.TrimSpace(in.Summary), Details: in.Details, IP: strings.TrimSpace(in.IP), CreatedAt: now, } m.mu.Lock() defer m.mu.Unlock() m.auditLog = append(m.auditLog, row) return cloneAuditEntry(row), nil } func (m *Memory) ListAudit(tenantID, cursor string, limit int, filter AuditListFilter) ([]*AuditEntry, string, bool, error) { if limit <= 0 { limit = 50 } m.mu.RLock() defer m.mu.RUnlock() var filtered []*AuditEntry for _, row := range m.auditLog { if row.TenantID != tenantID { continue } if a := strings.TrimSpace(filter.Action); a != "" && row.Action != a { continue } if s := strings.TrimSpace(filter.Severity); s != "" && row.Severity != s { continue } filtered = append(filtered, row) } sort.Slice(filtered, func(i, j int) bool { if filtered[i].CreatedAt.Equal(filtered[j].CreatedAt) { return filtered[i].ID > filtered[j].ID } return filtered[i].CreatedAt.After(filtered[j].CreatedAt) }) off := parseMaintCursor(cursor) end := off + limit next := "" hasMore := false if end > len(filtered) { end = len(filtered) } else if end < len(filtered) { hasMore = true next = formatMaintCursor(end) } if off >= len(filtered) { return nil, "", false, nil } out := make([]*AuditEntry, end-off) for i := off; i < end; i++ { out[i-off] = cloneAuditEntry(filtered[i]) } return out, next, hasMore, nil } func (m *Memory) MarkAuditPortalPushed(id string) error { m.mu.Lock() defer m.mu.Unlock() for _, row := range m.auditLog { if row.ID == id { now := time.Now().UTC() row.PortalPushedAt = &now return nil } } return ErrNotFound } func cloneAuditEntry(row *AuditEntry) *AuditEntry { if row == nil { return nil } cp := *row if row.Details != nil { cp.Details = make(map[string]any, len(row.Details)) for k, v := range row.Details { cp.Details[k] = v } } return &cp }