package store import ( "context" "fmt" "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo/options" "bountyboard/internal/ulid" ) // AuditEntry records one privileged mutation (ยง4.8). type AuditEntry struct { ID string `bson:"_id" json:"id"` At time.Time `bson:"at" json:"at"` ActorID string `bson:"actorId" json:"actorId"` ActorIP string `bson:"actorIp" json:"actorIp"` Action string `bson:"action" json:"action"` Entity string `bson:"entity" json:"entity"` EntityID string `bson:"entityId" json:"entityId"` Diff map[string]any `bson:"diff,omitempty" json:"diff,omitempty"` } // Audit appends to the audit log; failures are returned for logging but must // never abort the mutation they describe. func (s *Store) Audit(ctx context.Context, e AuditEntry) error { e.ID = ulid.New() e.At = time.Now().UTC() if _, err := s.DB.Collection("auditLog").InsertOne(ctx, e); err != nil { return fmt.Errorf("insert audit entry: %w", err) } return nil } // ListAudit returns newest-first entries with optional filters and ULID // cursor pagination. func (s *Store) ListAudit(ctx context.Context, actorID, entityID, cursor string, limit int) ([]AuditEntry, error) { if limit <= 0 || limit > 200 { limit = 50 } filter := bson.M{} if actorID != "" { filter["actorId"] = actorID } if entityID != "" { filter["entityId"] = entityID } if cursor != "" { filter["_id"] = bson.M{"$lt": cursor} } cur, err := s.DB.Collection("auditLog").Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "_id", Value: -1}}).SetLimit(int64(limit))) if err != nil { return nil, fmt.Errorf("list audit: %w", err) } out := []AuditEntry{} if err := cur.All(ctx, &out); err != nil { return nil, fmt.Errorf("decode audit: %w", err) } return out, nil }