189 lines
5.5 KiB
Go
189 lines
5.5 KiB
Go
package persistence
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/audit"
|
|
"RenewCMS/internal/infrastructure/persistence/mappers"
|
|
entity "RenewCMS/internal/infrastructure/persistence/models"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const likeEscape = " ESCAPE '!'"
|
|
|
|
type AuditEventRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
var _ audit.Repository = &AuditEventRepository{}
|
|
|
|
func NewAuditEventRepository(db *gorm.DB) *AuditEventRepository {
|
|
return &AuditEventRepository{db}
|
|
}
|
|
|
|
func (a *AuditEventRepository) Append(event audit.Event) (audit.Event, error) {
|
|
model, err := mappers.AuditEventToModel(event)
|
|
if err != nil {
|
|
return audit.Event{}, err
|
|
}
|
|
model.ID = 0
|
|
if err := a.db.Create(&model).Error; err != nil {
|
|
return audit.Event{}, err
|
|
}
|
|
return mappers.AuditEventToDomain(model)
|
|
}
|
|
|
|
func (a *AuditEventRepository) List(filter audit.Filter) (audit.Page, error) {
|
|
var total int64
|
|
if err := a.filtered(filter).Count(&total).Error; err != nil {
|
|
return audit.Page{}, err
|
|
}
|
|
|
|
query := a.filtered(filter).Order("created_at DESC, id DESC")
|
|
if filter.Size > 0 {
|
|
query = query.Limit(filter.Size).Offset(offsetOf(filter))
|
|
}
|
|
var found []entity.AuditEvent
|
|
if err := query.Find(&found).Error; err != nil {
|
|
return audit.Page{}, err
|
|
}
|
|
|
|
items := make([]audit.Event, 0, len(found))
|
|
for _, model := range found {
|
|
event, err := mappers.AuditEventToDomain(model)
|
|
if err != nil {
|
|
return audit.Page{}, err
|
|
}
|
|
items = append(items, event)
|
|
}
|
|
return audit.Page{Items: items, Total: int(total)}, nil
|
|
}
|
|
|
|
func offsetOf(filter audit.Filter) int {
|
|
if filter.Page < 1 {
|
|
return 0
|
|
}
|
|
return (filter.Page - 1) * filter.Size
|
|
}
|
|
|
|
func (a *AuditEventRepository) filtered(filter audit.Filter) *gorm.DB {
|
|
query := a.db.Model(&entity.AuditEvent{})
|
|
if filter.Actor != nil {
|
|
query = query.Where("actor_type = ?", string(filter.Actor.Type))
|
|
if filter.Actor.ID != nil {
|
|
query = query.Where("actor_id = ?", *filter.Actor.ID)
|
|
}
|
|
}
|
|
if filter.Category != "" {
|
|
query = query.Where("category = ?", string(filter.Category))
|
|
}
|
|
if filter.Outcome != "" {
|
|
query = query.Where("outcome = ?", string(filter.Outcome))
|
|
}
|
|
if filter.Security {
|
|
query = query.Where("security = ?", true)
|
|
}
|
|
if filter.From != nil {
|
|
query = query.Where("created_at >= ?", filter.From.UTC())
|
|
}
|
|
if filter.To != nil {
|
|
query = query.Where("created_at < ?", filter.To.UTC())
|
|
}
|
|
if filter.Query != "" {
|
|
pattern := containsPattern(strings.ToLower(filter.Query))
|
|
query = query.Where("(LOWER(actor_name) LIKE ?"+likeEscape+" OR LOWER(target_name) LIKE ?"+likeEscape+")", pattern, pattern)
|
|
}
|
|
return query
|
|
}
|
|
|
|
func (a *AuditEventRepository) Summary(query audit.SummaryQuery) (audit.Summary, error) {
|
|
var summary audit.Summary
|
|
totals, err := a.countsBetween(query.From, query.To)
|
|
if err != nil {
|
|
return audit.Summary{}, err
|
|
}
|
|
summary.Total, summary.Failed, summary.Security = totals.Total, totals.Failed, totals.Security
|
|
|
|
previous := query.Previous()
|
|
previousTotals, err := a.countsBetween(previous.From, previous.To)
|
|
if err != nil {
|
|
return audit.Summary{}, err
|
|
}
|
|
summary.PreviousTotal = previousTotals.Total
|
|
|
|
if summary.ActivePeople.Users, err = a.distinctActors(audit.ActorUser, query.From, query.To); err != nil {
|
|
return audit.Summary{}, err
|
|
}
|
|
if summary.ActivePeople.APIKeys, err = a.distinctActors(audit.ActorAPIKey, query.From, query.To); err != nil {
|
|
return audit.Summary{}, err
|
|
}
|
|
summary.Series, err = a.series(query)
|
|
return summary, err
|
|
}
|
|
|
|
type countsRow struct {
|
|
Total int
|
|
Failed int
|
|
Security int
|
|
}
|
|
|
|
func (a *AuditEventRepository) countsBetween(from time.Time, to time.Time) (countsRow, error) {
|
|
var row countsRow
|
|
err := a.db.Model(&entity.AuditEvent{}).
|
|
Select("COUNT(*) AS total, "+
|
|
"COALESCE(SUM(CASE WHEN outcome = ? THEN 1 ELSE 0 END), 0) AS failed, "+
|
|
"COALESCE(SUM(CASE WHEN security = ? THEN 1 ELSE 0 END), 0) AS security", string(audit.OutcomeFailed), true).
|
|
Where("created_at >= ? AND created_at < ?", from.UTC(), to.UTC()).
|
|
Scan(&row).Error
|
|
return row, err
|
|
}
|
|
|
|
func (a *AuditEventRepository) distinctActors(actorType audit.ActorType, from time.Time, to time.Time) (int, error) {
|
|
var count int64
|
|
err := a.db.Model(&entity.AuditEvent{}).
|
|
Where("actor_type = ? AND created_at >= ? AND created_at < ?", string(actorType), from.UTC(), to.UTC()).
|
|
Distinct("actor_id").
|
|
Count(&count).Error
|
|
return int(count), err
|
|
}
|
|
|
|
func (a *AuditEventRepository) series(query audit.SummaryQuery) (audit.Series, error) {
|
|
series := audit.Series{
|
|
Total: make([]int, audit.SeriesBuckets),
|
|
Active: make([]int, audit.SeriesBuckets),
|
|
Failed: make([]int, audit.SeriesBuckets),
|
|
Security: make([]int, audit.SeriesBuckets),
|
|
}
|
|
width := query.To.Sub(query.From) / audit.SeriesBuckets
|
|
if width <= 0 {
|
|
return series, nil
|
|
}
|
|
for i := 0; i < audit.SeriesBuckets; i++ {
|
|
from := query.From.Add(time.Duration(i) * width)
|
|
to := from.Add(width)
|
|
if i == audit.SeriesBuckets-1 {
|
|
to = query.To
|
|
}
|
|
counts, err := a.countsBetween(from, to)
|
|
if err != nil {
|
|
return audit.Series{}, err
|
|
}
|
|
users, err := a.distinctActors(audit.ActorUser, from, to)
|
|
if err != nil {
|
|
return audit.Series{}, err
|
|
}
|
|
keys, err := a.distinctActors(audit.ActorAPIKey, from, to)
|
|
if err != nil {
|
|
return audit.Series{}, err
|
|
}
|
|
series.Total[i], series.Failed[i], series.Security[i], series.Active[i] = counts.Total, counts.Failed, counts.Security, users+keys
|
|
}
|
|
return series, nil
|
|
}
|
|
|
|
func (a *AuditEventRepository) DeleteOlderThan(cutoff time.Time) (int, error) {
|
|
result := a.db.Where("created_at < ?", cutoff.UTC()).Delete(&entity.AuditEvent{})
|
|
return int(result.RowsAffected), result.Error
|
|
}
|