You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
119 lines
2.8 KiB
Go
119 lines
2.8 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"knowfoolery/backend/services/admin-service/internal/domain/audit"
|
|
sharedpostgres "knowfoolery/backend/shared/infra/database/postgres"
|
|
)
|
|
|
|
// AuditRepository implements audit.Repository using pgx.
|
|
type AuditRepository struct {
|
|
db *sharedpostgres.Client
|
|
}
|
|
|
|
// NewAuditRepository creates a PostgreSQL-backed audit repository.
|
|
func NewAuditRepository(db *sharedpostgres.Client) *AuditRepository {
|
|
return &AuditRepository{db: db}
|
|
}
|
|
|
|
func (r *AuditRepository) EnsureSchema(ctx context.Context) error {
|
|
q := `
|
|
CREATE TABLE IF NOT EXISTS admin_audit_logs (
|
|
id TEXT PRIMARY KEY,
|
|
at TIMESTAMPTZ NOT NULL,
|
|
actor_id TEXT NOT NULL,
|
|
actor_email TEXT NOT NULL,
|
|
action TEXT NOT NULL,
|
|
resource TEXT NOT NULL,
|
|
details JSONB NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_at ON admin_audit_logs(at DESC);
|
|
`
|
|
_, err := r.db.Pool.Exec(ctx, q)
|
|
return err
|
|
}
|
|
|
|
func (r *AuditRepository) Append(ctx context.Context, e audit.Entry) error {
|
|
var detailsJSON []byte
|
|
if e.Details != nil {
|
|
b, err := json.Marshal(e.Details)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal details: %w", err)
|
|
}
|
|
detailsJSON = b
|
|
}
|
|
|
|
_, err := r.db.Pool.Exec(ctx,
|
|
`INSERT INTO admin_audit_logs(id, at, actor_id, actor_email, action, resource, details)
|
|
VALUES($1,$2,$3,$4,$5,$6,$7)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
e.ID, e.At, e.ActorID, e.ActorEmail, e.Action, e.Resource, detailsJSON,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *AuditRepository) List(ctx context.Context, limit, offset int) ([]audit.Entry, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
rows, err := r.db.Pool.Query(ctx,
|
|
`SELECT id, at, actor_id, actor_email, action, resource, details
|
|
FROM admin_audit_logs
|
|
ORDER BY at DESC
|
|
LIMIT $1 OFFSET $2`, limit, offset,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []audit.Entry
|
|
for rows.Next() {
|
|
var e audit.Entry
|
|
var detailsBytes []byte
|
|
if err := rows.Scan(&e.ID, &e.At, &e.ActorID, &e.ActorEmail, &e.Action, &e.Resource, &detailsBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(detailsBytes) > 0 {
|
|
var v any
|
|
if err := json.Unmarshal(detailsBytes, &v); err == nil {
|
|
e.Details = v
|
|
}
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (r *AuditRepository) Count(ctx context.Context) (int64, error) {
|
|
var n int64
|
|
err := r.db.Pool.QueryRow(ctx, `SELECT COUNT(*) FROM admin_audit_logs`).Scan(&n)
|
|
return n, err
|
|
}
|
|
|
|
func (r *AuditRepository) PruneBefore(ctx context.Context, before time.Time) (int64, error) {
|
|
ct, err := r.db.Pool.Exec(ctx, `DELETE FROM admin_audit_logs WHERE at < $1`, before)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return ct.RowsAffected(), nil
|
|
}
|
|
|
|
var _ audit.Repository = (*AuditRepository)(nil)
|
|
|
|
// Compile-time check for pgx import.
|
|
var _ = pgx.ErrNoRows
|