activity

package
v0.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 21, 2026 License: MIT Imports: 13 Imported by: 5

README

Activity module

Audit logging helpers, a Bun-backed sink/repository, and query helpers for recent activity feeds and stats.

Components

  • BuildRecordFromActor and BuildRecordFromUUID convert request context into types.ActivityRecord with trimmed fields and cloned metadata.
  • Bun repository (activity.NewRepository) implements both types.ActivitySink and types.ActivityRepository for writes and reads.
  • Query handlers under query consume types.ActivityFilter/ActivityStatsFilter and respect tenant/org scope.

Constructing records

Use BuildRecordFromActor when you have a go-auth ActorContext (HTTP middleware); use BuildRecordFromUUID when you only have an actor UUID (background jobs, message handlers).

// From a request with go-auth metadata.
rec, err := activity.BuildRecordFromActor(actorCtx,
    "settings.updated",
    "settings",
    "global",
    map[string]any{"path": "ui.theme", "from": "light", "to": "dark"},
    activity.WithChannel("settings"),
)

// From a background worker with only an actor UUID.
rec, err := activity.BuildRecordFromUUID(actorID,
    "export.completed",
    "export.job",
    jobID,
    map[string]any{"format": "csv", "count": 120},
    activity.WithTenant(tenantID),
    activity.WithOrg(orgID),
    activity.WithOccurredAt(startedAt),
)

Record options:

  • WithChannel(string): module-level filter tag.
  • WithTenant(uuid.UUID): override tenant scope when not present in the actor context.
  • WithOrg(uuid.UUID): override org scope.
  • WithOccurredAt(time.Time): set a deterministic timestamp (defaults to time.Now().UTC()).

Wiring the sink/repository

store, err := activity.NewRepository(activity.RepositoryConfig{
    DB:    bunDB,
    Clock: types.SystemClock{},
    IDGen: types.UUIDGenerator{},
})
if err != nil {
    return err
}

svc := users.New(users.Config{
    ActivitySink:       store,
    ActivityRepository: store,
    // other dependencies...
})

if err := svc.ActivitySink.Log(ctx, rec); err != nil {
    return err
}

Log fills ID/OccurredAt when missing and persists to the user_activity table created by migration 000003_user_activity.sql.

Queries

feed, _ := svc.Queries().ActivityFeed.Query(ctx, types.ActivityFilter{
    Scope:      types.ScopeFilter{TenantID: tenantID},
    Channel:    "settings",
    Pagination: types.Pagination{Limit: 20},
})

stats, _ := svc.Queries().ActivityStats.Query(ctx, types.ActivityStatsFilter{
    Scope: types.ScopeFilter{TenantID: tenantID},
})

Filters include optional ActorID, UserID, ObjectType, ObjectID, Verb, Channel, Channels, ChannelDenylist, Since, and Until.

Role-aware filtering & sanitization

Use BuildFilterFromActor for role-aware filters or attach the default access policy to activity queries:

policy := activity.NewDefaultAccessPolicy(
    activity.WithPolicyFilterOptions(
        activity.WithChannelAllowlist("settings", "roles"),
        activity.WithMachineActivityEnabled(false),
        activity.WithSuperadminScope(true),
    ),
)

feedQuery := query.NewActivityFeedQuery(store, scopeGuard, query.WithActivityAccessPolicy(policy))
feed, _ := feedQuery.Query(ctx, types.ActivityFilter{
    Actor:      actorRef,
    Pagination: types.Pagination{Limit: 25},
})

Defaults treat system_admin/superadmin as superadmins and tenant_admin/admin/org_admin as admins; you can override with WithRoleAliases. Sanitization uses go-masker defaults and redacts IPs for non-superadmins by default.

Optional cursor pagination

For high-volume feeds, the cursor helper can paginate by created_at and id:

cursor := &activity.ActivityCursor{
    OccurredAt: lastRecord.OccurredAt,
    ID:         lastRecord.ID,
}
query := activity.ApplyCursorPagination(db.NewSelect().Model(&rows), cursor, 50)

Conventions

  • Verbs/objects: settings.updated (settings), export.completed (export.job), bulk.users.updated (bulk.job), media.uploaded (media.asset).
  • Channels: lowercase module names (settings, export, bulk, media) for dashboard filtering.
  • Metadata: flat, JSON-serializable keys; include counts and scope hints when relevant.

See docs/ACTIVITY.md for deeper guidance, indexes, and schema details.

Documentation

Overview

Package activity provides default persistence helpers for the go-users ActivitySink. The Repository implements both the sink (writes) and the ActivityRepository read-side contract so transports can log lifecycle events and later query them for dashboards. The ActivitySink interface lives in pkg/types and is intentionally minimal (`Log(ctx, ActivityRecord) error`) so hosts can swap sinks without breaking changes.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ApplyCursorPagination added in v0.10.0

func ApplyCursorPagination(q *bun.SelectQuery, cursor *ActivityCursor, limit int) *bun.SelectQuery

ApplyCursorPagination applies cursor pagination using created_at/id ordering. Results are ordered by created_at DESC, id DESC, and filtered to items older than the supplied cursor.

func BuildFilterFromActor added in v0.10.0

func BuildFilterFromActor(actor *auth.ActorContext, role string, req types.ActivityFilter, opts ...FilterOption) (types.ActivityFilter, error)

BuildFilterFromActor constructs a safe ActivityFilter using the auth actor context plus role-aware constraints and optional channel rules.

func BuildRecordFromActor

func BuildRecordFromActor(actor *auth.ActorContext, verb, objectType, objectID string, metadata map[string]any, opts ...RecordOption) (types.ActivityRecord, error)

BuildRecordFromActor constructs an ActivityRecord using the actor metadata supplied by go-auth middleware plus verb/object details and optional metadata. It normalizes actor, tenant, and org identifiers into UUIDs and defensively copies metadata to avoid caller mutation.

func BuildRecordFromUUID added in v0.3.0

func BuildRecordFromUUID(actorID uuid.UUID, verb, objectType, objectID string, metadata map[string]any, opts ...RecordOption) (types.ActivityRecord, error)

BuildRecordFromUUID constructs an ActivityRecord when only the actor UUID is available. It trims verb/object fields, validates required values, copies metadata defensively, and applies RecordOptions.

func DefaultAdminRoleAliases added in v0.10.0

func DefaultAdminRoleAliases() []string

DefaultAdminRoleAliases returns the default admin role aliases.

func DefaultMachineActorTypes added in v0.10.0

func DefaultMachineActorTypes() []string

DefaultMachineActorTypes returns the default machine actor type identifiers.

func DefaultMachineDataKeys added in v0.10.0

func DefaultMachineDataKeys() []string

DefaultMachineDataKeys returns the default machine data keys.

func DefaultMasker added in v0.10.0

func DefaultMasker() *masker.Masker

DefaultMasker returns a configured masker instance with the default denylist.

func DefaultSuperadminRoleAliases added in v0.10.0

func DefaultSuperadminRoleAliases() []string

DefaultSuperadminRoleAliases returns the default superadmin role aliases.

func SanitizeRecord added in v0.10.0

func SanitizeRecord(mask *masker.Masker, record types.ActivityRecord) types.ActivityRecord

SanitizeRecord masks sensitive values in the activity record data payload.

func SanitizeRecords added in v0.10.0

func SanitizeRecords(mask *masker.Masker, records []types.ActivityRecord) []types.ActivityRecord

SanitizeRecords masks sensitive values for every record in the slice.

func ToActivityRecord

func ToActivityRecord(entry *LogEntry) types.ActivityRecord

ToActivityRecord converts the Bun model into the domain activity record.

Types

type AccessPolicyOption added in v0.10.0

type AccessPolicyOption func(*DefaultAccessPolicy)

AccessPolicyOption customizes the default activity access policy.

func WithIPRedaction added in v0.10.0

func WithIPRedaction(enabled bool) AccessPolicyOption

WithIPRedaction toggles IP redaction for non-superadmin roles.

func WithPolicyFilterOptions added in v0.10.0

func WithPolicyFilterOptions(opts ...FilterOption) AccessPolicyOption

WithPolicyFilterOptions configures filter options applied during policy enforcement.

func WithPolicyMasker added in v0.10.0

func WithPolicyMasker(masker *masker.Masker) AccessPolicyOption

WithPolicyMasker overrides the masker used for sanitization.

func WithPolicyStatsSelfOnly added in v0.10.0

func WithPolicyStatsSelfOnly(enabled bool) AccessPolicyOption

WithPolicyStatsSelfOnly toggles self-only stats for non-admin roles.

type ActivityAccessPolicy added in v0.10.0

type ActivityAccessPolicy interface {
	Apply(actor *auth.ActorContext, role string, req types.ActivityFilter) (types.ActivityFilter, error)
	Sanitize(actor *auth.ActorContext, role string, records []types.ActivityRecord) []types.ActivityRecord
}

ActivityAccessPolicy applies role-aware constraints and sanitization to activity feeds.

type ActivityCursor added in v0.10.0

type ActivityCursor struct {
	OccurredAt time.Time
	ID         uuid.UUID
}

ActivityCursor defines the cursor shape for activity feeds.

type ActivityStatsPolicy added in v0.10.0

type ActivityStatsPolicy interface {
	ApplyStats(actor *auth.ActorContext, role string, req types.ActivityStatsFilter) (types.ActivityStatsFilter, error)
}

ActivityStatsPolicy applies role-aware constraints to activity stats.

type DefaultAccessPolicy added in v0.10.0

type DefaultAccessPolicy struct {
	// contains filtered or unexported fields
}

DefaultAccessPolicy applies BuildFilterFromActor and sanitizes records on read.

func NewDefaultAccessPolicy added in v0.10.0

func NewDefaultAccessPolicy(opts ...AccessPolicyOption) *DefaultAccessPolicy

NewDefaultAccessPolicy returns the default policy implementation.

func (*DefaultAccessPolicy) Apply added in v0.10.0

Apply enforces role-aware scope/visibility rules on the requested filter.

func (*DefaultAccessPolicy) ApplyStats added in v0.10.0

ApplyStats enforces role-aware scope/visibility rules on stats filters.

func (*DefaultAccessPolicy) Sanitize added in v0.10.0

func (p *DefaultAccessPolicy) Sanitize(actor *auth.ActorContext, role string, records []types.ActivityRecord) []types.ActivityRecord

Sanitize applies masking rules and IP redaction to activity records.

type FilterConfig added in v0.10.0

type FilterConfig struct {
	ChannelAllowlist []string
	ChannelDenylist  []string

	// Machine activity settings are reserved for policy layers that can inspect
	// records (actor type/data); BuildFilterFromActor does not use them directly.
	MachineActivityEnabled bool
	MachineActorTypes      []string
	MachineDataKeys        []string

	SuperadminScope bool

	AdminRoleAliases      []string
	SuperadminRoleAliases []string
}

FilterConfig controls how BuildFilterFromActor applies role and channel rules.

type FilterOption added in v0.10.0

type FilterOption func(*FilterConfig)

FilterOption mutates the filter configuration.

func WithChannelAllowlist added in v0.10.0

func WithChannelAllowlist(channels ...string) FilterOption

WithChannelAllowlist restricts results to the provided channels.

func WithChannelDenylist added in v0.10.0

func WithChannelDenylist(channels ...string) FilterOption

WithChannelDenylist excludes the provided channels.

func WithMachineActivityEnabled added in v0.10.0

func WithMachineActivityEnabled(enabled bool) FilterOption

WithMachineActivityEnabled toggles machine activity visibility.

func WithMachineActorTypes added in v0.10.0

func WithMachineActorTypes(types ...string) FilterOption

WithMachineActorTypes overrides the machine actor type identifiers.

func WithMachineDataKeys added in v0.10.0

func WithMachineDataKeys(keys ...string) FilterOption

WithMachineDataKeys overrides the data keys used to flag machine activity.

func WithRoleAliases added in v0.10.0

func WithRoleAliases(adminAliases, superadminAliases []string) FilterOption

WithRoleAliases overrides the admin/superadmin role alias lists.

func WithSuperadminScope added in v0.10.0

func WithSuperadminScope(enabled bool) FilterOption

WithSuperadminScope allows superadmins to widen scope beyond actor context.

type LogEntry

type LogEntry struct {
	bun.BaseModel `bun:"table:user_activity"`

	ID         uuid.UUID      `bun:",pk,type:uuid"`
	UserID     uuid.UUID      `bun:"user_id,type:uuid"`
	ActorID    uuid.UUID      `bun:"actor_id,type:uuid"`
	TenantID   uuid.UUID      `bun:"tenant_id,type:uuid"`
	OrgID      uuid.UUID      `bun:"org_id,type:uuid"`
	Verb       string         `bun:"verb"`
	ObjectType string         `bun:"object_type"`
	ObjectID   string         `bun:"object_id"`
	Channel    string         `bun:"channel"`
	IP         string         `bun:"ip"`
	Data       map[string]any `bun:"data,type:jsonb"`
	CreatedAt  time.Time      `bun:"created_at"`
}

LogEntry models the persisted row in user_activity.

func FromActivityRecord

func FromActivityRecord(record types.ActivityRecord) *LogEntry

FromActivityRecord converts a domain activity record into the Bun model so it can be reused by transports without duplicating conversion logic.

type RecordOption

type RecordOption func(*types.ActivityRecord)

RecordOption mutates the ActivityRecord produced by BuildRecordFromActor.

func WithChannel

func WithChannel(channel string) RecordOption

WithChannel sets the channel/module field used for downstream filtering.

func WithOccurredAt added in v0.3.0

func WithOccurredAt(occurredAt time.Time) RecordOption

WithOccurredAt overrides the default occurrence timestamp.

func WithOrg added in v0.3.0

func WithOrg(orgID uuid.UUID) RecordOption

WithOrg sets the organization identifier for the record.

func WithTenant added in v0.3.0

func WithTenant(tenantID uuid.UUID) RecordOption

WithTenant sets the tenant identifier for the record.

type Repository

type Repository struct {
	// contains filtered or unexported fields
}

Repository persists activity logs and exposes query helpers.

func NewRepository

func NewRepository(cfg RepositoryConfig) (*Repository, error)

NewRepository constructs a repository that implements both ActivitySink and ActivityRepository interfaces.

func (*Repository) ActivityStats

func (r *Repository) ActivityStats(ctx context.Context, filter types.ActivityStatsFilter) (types.ActivityStats, error)

ActivityStats aggregates counts grouped by verb.

func (*Repository) ListActivity

func (r *Repository) ListActivity(ctx context.Context, filter types.ActivityFilter) (types.ActivityPage, error)

ListActivity returns a paginated feed filtered by the supplied criteria.

func (*Repository) Log

func (r *Repository) Log(ctx context.Context, record types.ActivityRecord) error

Log persists an activity record into the database.

type RepositoryConfig

type RepositoryConfig struct {
	DB         *bun.DB
	Repository repository.Repository[*LogEntry]
	Clock      types.Clock
	IDGen      types.IDGenerator
}

RepositoryConfig wires the Bun-backed activity repository.

type SanitizerConfig added in v0.10.0

type SanitizerConfig struct {
	Masker *masker.Masker
}

SanitizerConfig controls the masker used for activity sanitization.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL