@@ -0,0 +1,99 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user