feat(admin): record and filter task creator IP

Tasks now capture the requesting client's IP on first persistence
(saveTaskToInventory sees the gin request context via a ClientIP
interface assertion, no gin import). The admin task list gains a
task_creator_ip condition with case-insensitive substring matching,
surfaced in the filter popover and task detail dialog.

Resolves #115

Authored By: TDvorak <info@tdvorak.dev>
pull/3580/head
Tomas Dvorak 2 weeks ago
parent 194dcbad2e
commit fca45cedd4

File diff suppressed because one or more lines are too long

@ -456,6 +456,7 @@ var (
{Name: "public_state", Type: field.TypeJSON},
{Name: "private_state", Type: field.TypeString, Nullable: true, Size: 2147483647},
{Name: "correlation_id", Type: field.TypeUUID, Nullable: true},
{Name: "creator_ip", Type: field.TypeString, Nullable: true, Size: 45},
{Name: "hidden", Type: field.TypeBool, Default: false},
{Name: "user_tasks", Type: field.TypeInt, Nullable: true},
}
@ -467,7 +468,7 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "tasks_users_tasks",
Columns: []*schema.Column{TasksColumns[10]},
Columns: []*schema.Column{TasksColumns[11]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.SetNull,
},

@ -14212,6 +14212,7 @@ type TaskMutation struct {
public_state **types.TaskPublicState
private_state *string
correlation_id *uuid.UUID
creator_ip *string
hidden *bool
clearedFields map[string]struct{}
user *int
@ -14695,6 +14696,55 @@ func (m *TaskMutation) ResetUserTasks() {
delete(m.clearedFields, task.FieldUserTasks)
}
// SetCreatorIP sets the "creator_ip" field.
func (m *TaskMutation) SetCreatorIP(s string) {
m.creator_ip = &s
}
// CreatorIP returns the value of the "creator_ip" field in the mutation.
func (m *TaskMutation) CreatorIP() (r string, exists bool) {
v := m.creator_ip
if v == nil {
return
}
return *v, true
}
// OldCreatorIP returns the old "creator_ip" field's value of the Task entity.
// If the Task object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *TaskMutation) OldCreatorIP(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldCreatorIP is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldCreatorIP requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldCreatorIP: %w", err)
}
return oldValue.CreatorIP, nil
}
// ClearCreatorIP clears the value of the "creator_ip" field.
func (m *TaskMutation) ClearCreatorIP() {
m.creator_ip = nil
m.clearedFields[task.FieldCreatorIP] = struct{}{}
}
// CreatorIPCleared returns if the "creator_ip" field was cleared in this mutation.
func (m *TaskMutation) CreatorIPCleared() bool {
_, ok := m.clearedFields[task.FieldCreatorIP]
return ok
}
// ResetCreatorIP resets all changes to the "creator_ip" field.
func (m *TaskMutation) ResetCreatorIP() {
m.creator_ip = nil
delete(m.clearedFields, task.FieldCreatorIP)
}
// SetHidden sets the "hidden" field.
func (m *TaskMutation) SetHidden(b bool) {
m.hidden = &b
@ -14805,7 +14855,7 @@ func (m *TaskMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *TaskMutation) Fields() []string {
fields := make([]string, 0, 10)
fields := make([]string, 0, 11)
if m.created_at != nil {
fields = append(fields, task.FieldCreatedAt)
}
@ -14833,6 +14883,9 @@ func (m *TaskMutation) Fields() []string {
if m.user != nil {
fields = append(fields, task.FieldUserTasks)
}
if m.creator_ip != nil {
fields = append(fields, task.FieldCreatorIP)
}
if m.hidden != nil {
fields = append(fields, task.FieldHidden)
}
@ -14862,6 +14915,8 @@ func (m *TaskMutation) Field(name string) (ent.Value, bool) {
return m.CorrelationID()
case task.FieldUserTasks:
return m.UserTasks()
case task.FieldCreatorIP:
return m.CreatorIP()
case task.FieldHidden:
return m.Hidden()
}
@ -14891,6 +14946,8 @@ func (m *TaskMutation) OldField(ctx context.Context, name string) (ent.Value, er
return m.OldCorrelationID(ctx)
case task.FieldUserTasks:
return m.OldUserTasks(ctx)
case task.FieldCreatorIP:
return m.OldCreatorIP(ctx)
case task.FieldHidden:
return m.OldHidden(ctx)
}
@ -14965,6 +15022,13 @@ func (m *TaskMutation) SetField(name string, value ent.Value) error {
}
m.SetUserTasks(v)
return nil
case task.FieldCreatorIP:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetCreatorIP(v)
return nil
case task.FieldHidden:
v, ok := value.(bool)
if !ok {
@ -15017,6 +15081,9 @@ func (m *TaskMutation) ClearedFields() []string {
if m.FieldCleared(task.FieldUserTasks) {
fields = append(fields, task.FieldUserTasks)
}
if m.FieldCleared(task.FieldCreatorIP) {
fields = append(fields, task.FieldCreatorIP)
}
return fields
}
@ -15043,6 +15110,9 @@ func (m *TaskMutation) ClearField(name string) error {
case task.FieldUserTasks:
m.ClearUserTasks()
return nil
case task.FieldCreatorIP:
m.ClearCreatorIP()
return nil
}
return fmt.Errorf("unknown Task nullable field %s", name)
}
@ -15078,6 +15148,9 @@ func (m *TaskMutation) ResetField(name string) error {
case task.FieldUserTasks:
m.ResetUserTasks()
return nil
case task.FieldCreatorIP:
m.ResetCreatorIP()
return nil
case task.FieldHidden:
m.ResetHidden()
return nil

@ -383,8 +383,12 @@ func init() {
task.DefaultUpdatedAt = taskDescUpdatedAt.Default.(func() time.Time)
// task.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
task.UpdateDefaultUpdatedAt = taskDescUpdatedAt.UpdateDefault.(func() time.Time)
// taskDescCreatorIP is the schema descriptor for creator_ip field.
taskDescCreatorIP := taskFields[6].Descriptor()
// task.CreatorIPValidator is a validator for the "creator_ip" field. It is called by the builders before save.
task.CreatorIPValidator = taskDescCreatorIP.Validators[0].(func(string) error)
// taskDescHidden is the schema descriptor for hidden field.
taskDescHidden := taskFields[6].Descriptor()
taskDescHidden := taskFields[7].Descriptor()
// task.DefaultHidden holds the default value on creation for the hidden field.
task.DefaultHidden = taskDescHidden.Default.(bool)
userMixin := schema.User{}.Mixin()

@ -26,6 +26,9 @@ func (Task) Fields() []ent.Field {
Optional().
Immutable(),
field.Int("user_tasks").Optional(),
// creator_ip records the client IP of the request that created the task,
// when the creation happened on an HTTP request context.
field.String("creator_ip").Optional().MaxLen(45),
// hidden is set when the owner deletes the task record; admin views keep it.
field.Bool("hidden").Default(false),
}

@ -39,6 +39,8 @@ type Task struct {
CorrelationID uuid.UUID `json:"correlation_id,omitempty"`
// UserTasks holds the value of the "user_tasks" field.
UserTasks int `json:"user_tasks,omitempty"`
// CreatorIP holds the value of the "creator_ip" field.
CreatorIP string `json:"creator_ip,omitempty"`
// Hidden holds the value of the "hidden" field.
Hidden bool `json:"hidden,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
@ -80,7 +82,7 @@ func (*Task) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullBool)
case task.FieldID, task.FieldUserTasks:
values[i] = new(sql.NullInt64)
case task.FieldType, task.FieldStatus, task.FieldPrivateState:
case task.FieldType, task.FieldStatus, task.FieldPrivateState, task.FieldCreatorIP:
values[i] = new(sql.NullString)
case task.FieldCreatedAt, task.FieldUpdatedAt, task.FieldDeletedAt:
values[i] = new(sql.NullTime)
@ -164,6 +166,12 @@ func (t *Task) assignValues(columns []string, values []any) error {
} else if value.Valid {
t.UserTasks = int(value.Int64)
}
case task.FieldCreatorIP:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field creator_ip", values[i])
} else if value.Valid {
t.CreatorIP = value.String
}
case task.FieldHidden:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field hidden", values[i])
@ -240,6 +248,9 @@ func (t *Task) String() string {
builder.WriteString("user_tasks=")
builder.WriteString(fmt.Sprintf("%v", t.UserTasks))
builder.WriteString(", ")
builder.WriteString("creator_ip=")
builder.WriteString(t.CreatorIP)
builder.WriteString(", ")
builder.WriteString("hidden=")
builder.WriteString(fmt.Sprintf("%v", t.Hidden))
builder.WriteByte(')')

@ -34,6 +34,8 @@ const (
FieldCorrelationID = "correlation_id"
// FieldUserTasks holds the string denoting the user_tasks field in the database.
FieldUserTasks = "user_tasks"
// FieldCreatorIP holds the string denoting the creator_ip field in the database.
FieldCreatorIP = "creator_ip"
// FieldHidden holds the string denoting the hidden field in the database.
FieldHidden = "hidden"
// EdgeUser holds the string denoting the user edge name in mutations.
@ -61,6 +63,7 @@ var Columns = []string{
FieldPrivateState,
FieldCorrelationID,
FieldUserTasks,
FieldCreatorIP,
FieldHidden,
}
@ -88,6 +91,8 @@ var (
DefaultUpdatedAt func() time.Time
// UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
UpdateDefaultUpdatedAt func() time.Time
// CreatorIPValidator is a validator for the "creator_ip" field. It is called by the builders before save.
CreatorIPValidator func(string) error
// DefaultHidden holds the default value on creation for the "hidden" field.
DefaultHidden bool
)
@ -170,6 +175,11 @@ func ByUserTasks(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldUserTasks, opts...).ToFunc()
}
// ByCreatorIP orders the results by the creator_ip field.
func ByCreatorIP(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldCreatorIP, opts...).ToFunc()
}
// ByHidden orders the results by the hidden field.
func ByHidden(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldHidden, opts...).ToFunc()

@ -91,6 +91,11 @@ func UserTasks(v int) predicate.Task {
return predicate.Task(sql.FieldEQ(FieldUserTasks, v))
}
// CreatorIP applies equality check predicate on the "creator_ip" field. It's identical to CreatorIPEQ.
func CreatorIP(v string) predicate.Task {
return predicate.Task(sql.FieldEQ(FieldCreatorIP, v))
}
// Hidden applies equality check predicate on the "hidden" field. It's identical to HiddenEQ.
func Hidden(v bool) predicate.Task {
return predicate.Task(sql.FieldEQ(FieldHidden, v))
@ -466,6 +471,81 @@ func UserTasksNotNil() predicate.Task {
return predicate.Task(sql.FieldNotNull(FieldUserTasks))
}
// CreatorIPEQ applies the EQ predicate on the "creator_ip" field.
func CreatorIPEQ(v string) predicate.Task {
return predicate.Task(sql.FieldEQ(FieldCreatorIP, v))
}
// CreatorIPNEQ applies the NEQ predicate on the "creator_ip" field.
func CreatorIPNEQ(v string) predicate.Task {
return predicate.Task(sql.FieldNEQ(FieldCreatorIP, v))
}
// CreatorIPIn applies the In predicate on the "creator_ip" field.
func CreatorIPIn(vs ...string) predicate.Task {
return predicate.Task(sql.FieldIn(FieldCreatorIP, vs...))
}
// CreatorIPNotIn applies the NotIn predicate on the "creator_ip" field.
func CreatorIPNotIn(vs ...string) predicate.Task {
return predicate.Task(sql.FieldNotIn(FieldCreatorIP, vs...))
}
// CreatorIPGT applies the GT predicate on the "creator_ip" field.
func CreatorIPGT(v string) predicate.Task {
return predicate.Task(sql.FieldGT(FieldCreatorIP, v))
}
// CreatorIPGTE applies the GTE predicate on the "creator_ip" field.
func CreatorIPGTE(v string) predicate.Task {
return predicate.Task(sql.FieldGTE(FieldCreatorIP, v))
}
// CreatorIPLT applies the LT predicate on the "creator_ip" field.
func CreatorIPLT(v string) predicate.Task {
return predicate.Task(sql.FieldLT(FieldCreatorIP, v))
}
// CreatorIPLTE applies the LTE predicate on the "creator_ip" field.
func CreatorIPLTE(v string) predicate.Task {
return predicate.Task(sql.FieldLTE(FieldCreatorIP, v))
}
// CreatorIPContains applies the Contains predicate on the "creator_ip" field.
func CreatorIPContains(v string) predicate.Task {
return predicate.Task(sql.FieldContains(FieldCreatorIP, v))
}
// CreatorIPHasPrefix applies the HasPrefix predicate on the "creator_ip" field.
func CreatorIPHasPrefix(v string) predicate.Task {
return predicate.Task(sql.FieldHasPrefix(FieldCreatorIP, v))
}
// CreatorIPHasSuffix applies the HasSuffix predicate on the "creator_ip" field.
func CreatorIPHasSuffix(v string) predicate.Task {
return predicate.Task(sql.FieldHasSuffix(FieldCreatorIP, v))
}
// CreatorIPIsNil applies the IsNil predicate on the "creator_ip" field.
func CreatorIPIsNil() predicate.Task {
return predicate.Task(sql.FieldIsNull(FieldCreatorIP))
}
// CreatorIPNotNil applies the NotNil predicate on the "creator_ip" field.
func CreatorIPNotNil() predicate.Task {
return predicate.Task(sql.FieldNotNull(FieldCreatorIP))
}
// CreatorIPEqualFold applies the EqualFold predicate on the "creator_ip" field.
func CreatorIPEqualFold(v string) predicate.Task {
return predicate.Task(sql.FieldEqualFold(FieldCreatorIP, v))
}
// CreatorIPContainsFold applies the ContainsFold predicate on the "creator_ip" field.
func CreatorIPContainsFold(v string) predicate.Task {
return predicate.Task(sql.FieldContainsFold(FieldCreatorIP, v))
}
// HiddenEQ applies the EQ predicate on the "hidden" field.
func HiddenEQ(v bool) predicate.Task {
return predicate.Task(sql.FieldEQ(FieldHidden, v))

@ -135,6 +135,20 @@ func (tc *TaskCreate) SetNillableUserTasks(i *int) *TaskCreate {
return tc
}
// SetCreatorIP sets the "creator_ip" field.
func (tc *TaskCreate) SetCreatorIP(s string) *TaskCreate {
tc.mutation.SetCreatorIP(s)
return tc
}
// SetNillableCreatorIP sets the "creator_ip" field if the given value is not nil.
func (tc *TaskCreate) SetNillableCreatorIP(s *string) *TaskCreate {
if s != nil {
tc.SetCreatorIP(*s)
}
return tc
}
// SetHidden sets the "hidden" field.
func (tc *TaskCreate) SetHidden(b bool) *TaskCreate {
tc.mutation.SetHidden(b)
@ -252,6 +266,11 @@ func (tc *TaskCreate) check() error {
if _, ok := tc.mutation.PublicState(); !ok {
return &ValidationError{Name: "public_state", err: errors.New(`ent: missing required field "Task.public_state"`)}
}
if v, ok := tc.mutation.CreatorIP(); ok {
if err := task.CreatorIPValidator(v); err != nil {
return &ValidationError{Name: "creator_ip", err: fmt.Errorf(`ent: validator failed for field "Task.creator_ip": %w`, err)}
}
}
if _, ok := tc.mutation.Hidden(); !ok {
return &ValidationError{Name: "hidden", err: errors.New(`ent: missing required field "Task.hidden"`)}
}
@ -321,6 +340,10 @@ func (tc *TaskCreate) createSpec() (*Task, *sqlgraph.CreateSpec) {
_spec.SetField(task.FieldCorrelationID, field.TypeUUID, value)
_node.CorrelationID = value
}
if value, ok := tc.mutation.CreatorIP(); ok {
_spec.SetField(task.FieldCreatorIP, field.TypeString, value)
_node.CreatorIP = value
}
if value, ok := tc.mutation.Hidden(); ok {
_spec.SetField(task.FieldHidden, field.TypeBool, value)
_node.Hidden = value
@ -496,6 +519,24 @@ func (u *TaskUpsert) ClearUserTasks() *TaskUpsert {
return u
}
// SetCreatorIP sets the "creator_ip" field.
func (u *TaskUpsert) SetCreatorIP(v string) *TaskUpsert {
u.Set(task.FieldCreatorIP, v)
return u
}
// UpdateCreatorIP sets the "creator_ip" field to the value that was provided on create.
func (u *TaskUpsert) UpdateCreatorIP() *TaskUpsert {
u.SetExcluded(task.FieldCreatorIP)
return u
}
// ClearCreatorIP clears the value of the "creator_ip" field.
func (u *TaskUpsert) ClearCreatorIP() *TaskUpsert {
u.SetNull(task.FieldCreatorIP)
return u
}
// SetHidden sets the "hidden" field.
func (u *TaskUpsert) SetHidden(v bool) *TaskUpsert {
u.Set(task.FieldHidden, v)
@ -675,6 +716,27 @@ func (u *TaskUpsertOne) ClearUserTasks() *TaskUpsertOne {
})
}
// SetCreatorIP sets the "creator_ip" field.
func (u *TaskUpsertOne) SetCreatorIP(v string) *TaskUpsertOne {
return u.Update(func(s *TaskUpsert) {
s.SetCreatorIP(v)
})
}
// UpdateCreatorIP sets the "creator_ip" field to the value that was provided on create.
func (u *TaskUpsertOne) UpdateCreatorIP() *TaskUpsertOne {
return u.Update(func(s *TaskUpsert) {
s.UpdateCreatorIP()
})
}
// ClearCreatorIP clears the value of the "creator_ip" field.
func (u *TaskUpsertOne) ClearCreatorIP() *TaskUpsertOne {
return u.Update(func(s *TaskUpsert) {
s.ClearCreatorIP()
})
}
// SetHidden sets the "hidden" field.
func (u *TaskUpsertOne) SetHidden(v bool) *TaskUpsertOne {
return u.Update(func(s *TaskUpsert) {
@ -1027,6 +1089,27 @@ func (u *TaskUpsertBulk) ClearUserTasks() *TaskUpsertBulk {
})
}
// SetCreatorIP sets the "creator_ip" field.
func (u *TaskUpsertBulk) SetCreatorIP(v string) *TaskUpsertBulk {
return u.Update(func(s *TaskUpsert) {
s.SetCreatorIP(v)
})
}
// UpdateCreatorIP sets the "creator_ip" field to the value that was provided on create.
func (u *TaskUpsertBulk) UpdateCreatorIP() *TaskUpsertBulk {
return u.Update(func(s *TaskUpsert) {
s.UpdateCreatorIP()
})
}
// ClearCreatorIP clears the value of the "creator_ip" field.
func (u *TaskUpsertBulk) ClearCreatorIP() *TaskUpsertBulk {
return u.Update(func(s *TaskUpsert) {
s.ClearCreatorIP()
})
}
// SetHidden sets the "hidden" field.
func (u *TaskUpsertBulk) SetHidden(v bool) *TaskUpsertBulk {
return u.Update(func(s *TaskUpsert) {

@ -130,6 +130,26 @@ func (tu *TaskUpdate) ClearUserTasks() *TaskUpdate {
return tu
}
// SetCreatorIP sets the "creator_ip" field.
func (tu *TaskUpdate) SetCreatorIP(s string) *TaskUpdate {
tu.mutation.SetCreatorIP(s)
return tu
}
// SetNillableCreatorIP sets the "creator_ip" field if the given value is not nil.
func (tu *TaskUpdate) SetNillableCreatorIP(s *string) *TaskUpdate {
if s != nil {
tu.SetCreatorIP(*s)
}
return tu
}
// ClearCreatorIP clears the value of the "creator_ip" field.
func (tu *TaskUpdate) ClearCreatorIP() *TaskUpdate {
tu.mutation.ClearCreatorIP()
return tu
}
// SetHidden sets the "hidden" field.
func (tu *TaskUpdate) SetHidden(b bool) *TaskUpdate {
tu.mutation.SetHidden(b)
@ -223,6 +243,11 @@ func (tu *TaskUpdate) check() error {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Task.status": %w`, err)}
}
}
if v, ok := tu.mutation.CreatorIP(); ok {
if err := task.CreatorIPValidator(v); err != nil {
return &ValidationError{Name: "creator_ip", err: fmt.Errorf(`ent: validator failed for field "Task.creator_ip": %w`, err)}
}
}
return nil
}
@ -265,6 +290,12 @@ func (tu *TaskUpdate) sqlSave(ctx context.Context) (n int, err error) {
if tu.mutation.CorrelationIDCleared() {
_spec.ClearField(task.FieldCorrelationID, field.TypeUUID)
}
if value, ok := tu.mutation.CreatorIP(); ok {
_spec.SetField(task.FieldCreatorIP, field.TypeString, value)
}
if tu.mutation.CreatorIPCleared() {
_spec.ClearField(task.FieldCreatorIP, field.TypeString)
}
if value, ok := tu.mutation.Hidden(); ok {
_spec.SetField(task.FieldHidden, field.TypeBool, value)
}
@ -417,6 +448,26 @@ func (tuo *TaskUpdateOne) ClearUserTasks() *TaskUpdateOne {
return tuo
}
// SetCreatorIP sets the "creator_ip" field.
func (tuo *TaskUpdateOne) SetCreatorIP(s string) *TaskUpdateOne {
tuo.mutation.SetCreatorIP(s)
return tuo
}
// SetNillableCreatorIP sets the "creator_ip" field if the given value is not nil.
func (tuo *TaskUpdateOne) SetNillableCreatorIP(s *string) *TaskUpdateOne {
if s != nil {
tuo.SetCreatorIP(*s)
}
return tuo
}
// ClearCreatorIP clears the value of the "creator_ip" field.
func (tuo *TaskUpdateOne) ClearCreatorIP() *TaskUpdateOne {
tuo.mutation.ClearCreatorIP()
return tuo
}
// SetHidden sets the "hidden" field.
func (tuo *TaskUpdateOne) SetHidden(b bool) *TaskUpdateOne {
tuo.mutation.SetHidden(b)
@ -523,6 +574,11 @@ func (tuo *TaskUpdateOne) check() error {
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Task.status": %w`, err)}
}
}
if v, ok := tuo.mutation.CreatorIP(); ok {
if err := task.CreatorIPValidator(v); err != nil {
return &ValidationError{Name: "creator_ip", err: fmt.Errorf(`ent: validator failed for field "Task.creator_ip": %w`, err)}
}
}
return nil
}
@ -582,6 +638,12 @@ func (tuo *TaskUpdateOne) sqlSave(ctx context.Context) (_node *Task, err error)
if tuo.mutation.CorrelationIDCleared() {
_spec.ClearField(task.FieldCorrelationID, field.TypeUUID)
}
if value, ok := tuo.mutation.CreatorIP(); ok {
_spec.SetField(task.FieldCreatorIP, field.TypeString, value)
}
if tuo.mutation.CreatorIPCleared() {
_spec.ClearField(task.FieldCreatorIP, field.TypeString)
}
if value, ok := tuo.mutation.Hidden(); ok {
_spec.SetField(task.FieldHidden, field.TypeBool, value)
}

@ -539,6 +539,7 @@ export interface Task extends CommonMixin {
public_state?: TaskPublicState;
private_state?: string;
correlation_id?: string;
creator_ip?: string;
user_tasks?: number;
edges: {
user?: User;

@ -157,6 +157,12 @@ const TaskForm = ({ values }: { values: Task }) => {
</Typography>
</SettingForm>
<SettingForm title={t("event.ip")} noContainer lgWidth={4}>
<Typography variant={"body2"} color={"textSecondary"}>
{values?.creator_ip || "-"}
</Typography>
</SettingForm>
<SettingForm title={t("application:setting.executeDuration")} noContainer lgWidth={2}>
<Typography variant={"body2"} color={"textSecondary"}>
{formatDuration(dayjs.duration((values?.public_state?.executed_duration ?? 0) / 1e6))}

@ -14,6 +14,8 @@ export interface TaskFilterPopoverProps extends PopoverProps {
setUser: (user: string) => void;
correlationID: string;
setCorrelationID: (correlationID: string) => void;
creatorIP: string;
setCreatorIP: (creatorIP: string) => void;
type: string;
setType: (type: string) => void;
clearFilters: () => void;
@ -26,6 +28,8 @@ const TaskFilterPopover = ({
setUser,
correlationID,
setCorrelationID,
creatorIP,
setCreatorIP,
type,
setType,
clearFilters,
@ -39,6 +43,7 @@ const TaskFilterPopover = ({
const [localStatus, setLocalStatus] = useState(status);
const [localUser, setLocalUser] = useState(user);
const [localCorrelationID, setLocalCorrelationID] = useState(correlationID);
const [localCreatorIP, setLocalCreatorIP] = useState(creatorIP);
const [localType, setLocalType] = useState(type);
// Initialize local state when popup opens
@ -47,6 +52,7 @@ const TaskFilterPopover = ({
setLocalStatus(status);
setLocalUser(user);
setLocalCorrelationID(correlationID);
setLocalCreatorIP(creatorIP);
setLocalType(type);
}
}, [open]);
@ -56,6 +62,7 @@ const TaskFilterPopover = ({
setStatus(localStatus);
setUser(localUser);
setCorrelationID(localCorrelationID);
setCreatorIP(localCreatorIP);
setType(localType);
onClose?.({}, "backdropClick");
};
@ -65,6 +72,7 @@ const TaskFilterPopover = ({
setLocalStatus("");
setLocalUser("");
setLocalCorrelationID("");
setLocalCreatorIP("");
setLocalType("");
clearFilters();
onClose?.({}, "backdropClick");
@ -114,6 +122,16 @@ const TaskFilterPopover = ({
/>
</SettingForm>
<SettingForm title={t("event.ip")} noContainer lgWidth={12}>
<DenseFilledTextField
fullWidth
value={localCreatorIP}
onChange={(e) => setLocalCreatorIP(e.target.value)}
placeholder={t("user.emptyNoFilter")}
size="small"
/>
</SettingForm>
<SettingForm title={t("task.type")} noContainer lgWidth={12}>
<DenseSelect
fullWidth

@ -45,6 +45,7 @@ export const UserQuery = "user";
export const TypeQuery = "type";
export const StatusQuery = "status";
export const CorrelationIDQuery = "correlation_id";
export const CreatorIPQuery = "creator_ip";
const TaskList = () => {
const { t } = useTranslation("dashboard");
@ -65,6 +66,7 @@ const TaskList = () => {
const [type, setType] = useQueryState(TypeQuery, { defaultValue: "" });
const [status, setStatus] = useQueryState(StatusQuery, { defaultValue: "" });
const [correlationID, setCorrelationID] = useQueryState(CorrelationIDQuery, { defaultValue: "" });
const [creatorIP, setCreatorIP] = useQueryState(CreatorIPQuery, { defaultValue: "" });
const [count, setCount] = useState(0);
const [selected, setSelected] = useState<readonly number[]>([]);
@ -92,11 +94,12 @@ const TaskList = () => {
setType("");
setStatus("");
setCorrelationID("");
}, [setUser, setType, setStatus, setCorrelationID]);
setCreatorIP("");
}, [setUser, setType, setStatus, setCorrelationID, setCreatorIP]);
useEffect(() => {
fetchTasks();
}, [page, pageSize, orderBy, orderDirection, user, type, status, correlationID]);
}, [page, pageSize, orderBy, orderDirection, user, type, status, correlationID, creatorIP]);
const fetchTasks = () => {
setLoading(true);
@ -112,6 +115,7 @@ const TaskList = () => {
task_user_id: user,
task_type: type,
task_correlation_id: correlationID,
task_creator_ip: creatorIP,
},
};
@ -227,6 +231,8 @@ const TaskList = () => {
setUser={setUser}
correlationID={correlationID}
setCorrelationID={setCorrelationID}
creatorIP={creatorIP}
setCreatorIP={setCreatorIP}
type={type}
setType={setType}
clearFilters={clearFilters}

@ -26,6 +26,7 @@ type (
PrivateState string
OwnerID int
CorrelationID uuid.UUID
CreatorIP string
}
)
@ -61,6 +62,8 @@ type (
Status []task.Status
UserID int
CorrelationID *uuid.UUID
// CreatorIP filters tasks created from a matching client IP (substring).
CreatorIP string
// ExcludeHidden filters out tasks hidden by their owner.
ExcludeHidden bool
}
@ -116,6 +119,10 @@ func (c *taskClient) New(ctx context.Context, task *TaskArgs) (*ent.Task, error)
stm.SetCorrelationID(task.CorrelationID)
}
if task.CreatorIP != "" {
stm.SetCreatorIP(task.CreatorIP)
}
newTask, err := stm.Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create task: %w", err)
@ -240,6 +247,10 @@ func (c *taskClient) List(ctx context.Context, args *ListTaskArgs) (*ListTaskRes
q.Where(task.CorrelationID(*args.CorrelationID))
}
if args.CreatorIP != "" {
q.Where(task.CreatorIPContainsFold(args.CreatorIP))
}
if args.ExcludeHidden {
q.Where(task.Hidden(false))
}

@ -531,6 +531,9 @@ func saveTaskToInventory(ctx context.Context, task Task, newStatus task.Status,
PrivateState: task.State(),
OwnerID: task.Owner().ID,
CorrelationID: logging.CorrelationID(ctx),
// The first persistence happens on the request's context when the task
// is submitted from an HTTP handler; gin.Context satisfies ClientIP.
CreatorIP: clientIPFromContext(ctx),
}
var (
@ -550,3 +553,15 @@ func saveTaskToInventory(ctx context.Context, task Task, newStatus task.Status,
task.OnPersisted(res)
return nil
}
// clientIPFromContext extracts the client IP when ctx is an HTTP request
// context (gin.Context implements ClientIP()). Worker contexts return "".
func clientIPFromContext(ctx context.Context) string {
type clientIPer interface {
ClientIP() string
}
if c, ok := ctx.(clientIPer); ok && c != nil {
return c.ClientIP()
}
return ""
}

@ -77,6 +77,7 @@ const (
taskStatusCondition = "task_status"
taskCorrelationIDCondition = "task_correlation_id"
taskUserIDCondition = "task_user_id"
taskCreatorIPCondition = "task_creator_ip"
)
func (s *AdminListService) Tasks(c *gin.Context) (*ListTaskResponse, error) {
@ -124,6 +125,7 @@ func (s *AdminListService) Tasks(c *gin.Context) (*ListTaskResponse, error) {
},
UserID: userID,
CorrelationID: correlationID,
CreatorIP: s.Conditions[taskCreatorIPCondition],
Types: taskType,
Status: status,
})

Loading…
Cancel
Save