Files
EvoBGP/internal/store/memory_audit.go
T
DenozordecandCursor 738d2e2256
CI / changes (push) Successful in 7s
CI / commitlint (push) Skipped
CI / web (push) Skipped
CI / openapi (push) Successful in 32s
CI / go (push) Successful in 1m23s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 4m56s
feat(httpapi): add local audit log with portal dual-write
Локальный audit_log (миграции pg/sqlite), GET /v1/audit, запись на CRUD и async push в auth-portal (source_app=bgp).

Co-authored-by: Cursor <[email protected]>
2026-07-21 13:24:54 +07:00

118 lines
3.0 KiB
Go

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
}