100 lines
2.6 KiB
Go
100 lines
2.6 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Entry struct {
|
|
ID int64 `json:"id"`
|
|
UserID *int64 `json:"user_id,omitempty"`
|
|
Actor string `json:"actor"`
|
|
Action string `json:"action"`
|
|
Resource string `json:"resource"`
|
|
Detail map[string]any `json:"detail,omitempty"`
|
|
IP string `json:"ip"`
|
|
UserAgent string `json:"user_agent"`
|
|
Status int `json:"status"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
}
|
|
|
|
type Service struct{ db *sql.DB }
|
|
|
|
func New(db *sql.DB) *Service { return &Service{db: db} }
|
|
|
|
func (s *Service) Log(ctx context.Context, e Entry) error {
|
|
if e.CreatedAt == 0 {
|
|
e.CreatedAt = time.Now().Unix()
|
|
}
|
|
b, _ := json.Marshal(e.Detail)
|
|
_, err := s.db.ExecContext(ctx, `INSERT INTO audit_log(user_id,actor,action,resource,detail_json,ip,user_agent,status,created_at) VALUES(?,?,?,?,?,?,?,?,?)`, e.UserID, e.Actor, e.Action, e.Resource, string(b), e.IP, e.UserAgent, e.Status, e.CreatedAt)
|
|
return err
|
|
}
|
|
func (s *Service) List(ctx context.Context, limit, offset int, action string) ([]Entry, error) {
|
|
if limit < 1 {
|
|
limit = 100
|
|
}
|
|
if limit > 500 {
|
|
limit = 500
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
q := `SELECT id,user_id,actor,action,resource,detail_json,ip,user_agent,status,created_at FROM audit_log`
|
|
args := []any{}
|
|
if strings.TrimSpace(action) != "" {
|
|
q += ` WHERE action LIKE ?`
|
|
args = append(args, "%"+strings.TrimSpace(action)+"%")
|
|
}
|
|
q += ` ORDER BY id DESC LIMIT ? OFFSET ?`
|
|
args = append(args, limit, offset)
|
|
rows, err := s.db.QueryContext(ctx, q, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := []Entry{}
|
|
for rows.Next() {
|
|
var e Entry
|
|
var uid sql.NullInt64
|
|
var raw string
|
|
if err := rows.Scan(&e.ID, &uid, &e.Actor, &e.Action, &e.Resource, &raw, &e.IP, &e.UserAgent, &e.Status, &e.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if uid.Valid {
|
|
v := uid.Int64
|
|
e.UserID = &v
|
|
}
|
|
_ = json.Unmarshal([]byte(raw), &e.Detail)
|
|
out = append(out, e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Run periodically prunes old audit records. A retention of 0 keeps the audit
|
|
// trail indefinitely.
|
|
func (s *Service) Run(ctx context.Context, retentionDays int) {
|
|
if retentionDays <= 0 {
|
|
<-ctx.Done()
|
|
return
|
|
}
|
|
cleanup := func() {
|
|
cut := time.Now().Add(-time.Duration(retentionDays) * 24 * time.Hour).Unix()
|
|
_, _ = s.db.ExecContext(ctx, `DELETE FROM audit_log WHERE created_at<?`, cut)
|
|
}
|
|
cleanup()
|
|
t := time.NewTicker(24 * time.Hour)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
cleanup()
|
|
}
|
|
}
|
|
}
|