458ba6a7ee
- customer CRUD wizard backend: per-system credential shapes validated via
connector construction, AES-256-GCM at rest, write-only over the API
- ticketing connectors (jira/azure_devops/youtrack/demo) with test-connection
- user management: roles, disable (revokes sessions), force password reset,
delete; self-demotion/disable/delete guards
- admin-editable runtime settings ({_id:app}) incl. atomizer URL override
- audit log written on every privileged mutation + filterable list API
- service status panel: mongo/atomizer/work-performer health + latency with
late-bound breaker, sync and jobs status providers
- tabbed admin UI (customers wizard dialog, users table, settings, status, audit)
- compose: mongo nofile ulimit 64000 (1024 default crashed WiredTiger)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
64 lines
1.8 KiB
Go
64 lines
1.8 KiB
Go
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
|
|
}
|