package repository import ( "context" "database/sql" "fmt" "strings" "time" ) type ActivityLogRow struct { CreatedAt time.Time `json:"created_at"` RequestStartedAt *time.Time `json:"request_started_at,omitempty"` RequestFinishedAt *time.Time `json:"request_finished_at,omitempty"` DurationMs *int `json:"duration_ms,omitempty"` HttpStatus *int `json:"http_status,omitempty"` Username string `json:"username"` RoleCode string `json:"role_code"` ActionCategory string `json:"action_category"` ActionType string `json:"action_type"` ActionTarget string `json:"action_target"` Description string `json:"description"` // ✅ TARGET USER TargetDfUsrID int64 `json:"target_dfusr_id"` TargetUsername string `json:"target_username"` // ✅ DIFF ChangeBefore string `json:"change_before"` ChangeAfter string `json:"change_after"` IpAddress string `json:"ip_address"` IsSuccess bool `json:"is_success"` ErrorMsg string `json:"error_message"` UserAgent string `json:"user_agent"` SessionID string `json:"session_id"` // audit ids UserUUID string `json:"user_id"` DfUsrID int64 `json:"dfusr_id"` Email string `json:"email"` IsActive bool `json:"is_active"` } type ActivityLogQuery struct { Username string RoleCode string ActionCategory string ActionType string ActionTarget string Success *bool StatusMin *int StatusMax *int DateFrom *time.Time DateTo *time.Time Page int Limit int } type ActivityLogResult struct { Total int64 `json:"total"` Items []ActivityLogRow `json:"items"` } func ListActivityLogs( ctx context.Context, db *sql.DB, q ActivityLogQuery, ) (ActivityLogResult, error) { // ---------- defaults ---------- if q.Page <= 0 { q.Page = 1 } // limit <=0 → unlimited if q.Limit < 0 { q.Limit = 50 } if q.Limit > 200 { q.Limit = 200 } offset := 0 if q.Limit > 0 { offset = (q.Page - 1) * q.Limit } where := []string{} args := []interface{}{} add := func(cond string, val interface{}) { where = append(where, cond) args = append(args, val) } // ---------- filters ---------- if strings.TrimSpace(q.Username) != "" { add(fmt.Sprintf("l.username ILIKE $%d", len(args)+1), "%"+strings.TrimSpace(q.Username)+"%") } if strings.TrimSpace(q.RoleCode) != "" { add(fmt.Sprintf("r.code = $%d", len(args)+1), strings.TrimSpace(q.RoleCode)) } if strings.TrimSpace(q.ActionCategory) != "" { add(fmt.Sprintf("l.action_category = $%d", len(args)+1), strings.TrimSpace(q.ActionCategory)) } if strings.TrimSpace(q.ActionType) != "" { add(fmt.Sprintf("l.action_type = $%d", len(args)+1), strings.TrimSpace(q.ActionType)) } if strings.TrimSpace(q.ActionTarget) != "" { add(fmt.Sprintf("l.action_target ILIKE $%d", len(args)+1), "%"+strings.TrimSpace(q.ActionTarget)+"%") } if q.Success != nil { add(fmt.Sprintf("l.is_success = $%d", len(args)+1), *q.Success) } if q.StatusMin != nil { add(fmt.Sprintf("l.http_status >= $%d", len(args)+1), *q.StatusMin) } if q.StatusMax != nil { add(fmt.Sprintf("l.http_status <= $%d", len(args)+1), *q.StatusMax) } if q.DateFrom != nil { add(fmt.Sprintf("l.created_at >= $%d", len(args)+1), *q.DateFrom) } if q.DateTo != nil { add(fmt.Sprintf("l.created_at < $%d", len(args)+1), q.DateTo.Add(24*time.Hour)) } whereSQL := "" if len(where) > 0 { whereSQL = "WHERE " + strings.Join(where, " AND ") } // ---------- COUNT ---------- countSQL := fmt.Sprintf(` SELECT count(*) FROM mk_user_activity_log l LEFT JOIN mk_dfusr u ON u.id = l.dfusr_id LEFT JOIN dfrole_usr ru ON ru.dfusr_id = u.id LEFT JOIN dfrole r ON r.id = ru.dfrole_id %s `, whereSQL) var total int64 if err := db.QueryRowContext(ctx, countSQL, args...).Scan(&total); err != nil { return ActivityLogResult{}, err } // ---------- LIST ---------- listArgs := append([]interface{}{}, args...) listSQL := fmt.Sprintf(` SELECT l.created_at, l.request_started_at, l.request_finished_at, l.duration_ms, l.http_status, COALESCE(u.username, l.username, '') AS username, COALESCE(r.code,''), COALESCE(l.action_category,''), COALESCE(l.action_type,''), COALESCE(l.action_target,''), COALESCE(l.description,''), COALESCE(l.target_dfusr_id, 0), COALESCE(l.target_username, ''), COALESCE(l.change_before::text, ''), COALESCE(l.change_after::text, ''), COALESCE(l.ip_address,''), COALESCE(l.is_success,false), COALESCE(l.error_message,''), COALESCE(l.user_agent,''), COALESCE(l.session_id,''), COALESCE(l.user_id::text,''), COALESCE(l.dfusr_id,0), COALESCE(u.email,''), COALESCE(u.is_active,false) FROM mk_user_activity_log l LEFT JOIN mk_dfusr u ON u.id = l.dfusr_id LEFT JOIN dfrole_usr ru ON ru.dfusr_id = u.id LEFT JOIN dfrole r ON r.id = ru.dfrole_id %s ORDER BY l.created_at DESC `, whereSQL) // LIMIT sadece istenirse if q.Limit > 0 { listSQL += fmt.Sprintf( " LIMIT $%d OFFSET $%d", len(listArgs)+1, len(listArgs)+2, ) listArgs = append(listArgs, q.Limit, offset) } rows, err := db.QueryContext(ctx, listSQL, listArgs...) if err != nil { return ActivityLogResult{}, err } defer rows.Close() items := []ActivityLogRow{} for rows.Next() { var it ActivityLogRow var started, finished sql.NullTime var dur, status sql.NullInt64 if err := rows.Scan( &it.CreatedAt, &started, &finished, &dur, &status, &it.Username, &it.RoleCode, &it.ActionCategory, &it.ActionType, &it.ActionTarget, &it.Description, // ✅ NEW &it.TargetDfUsrID, &it.TargetUsername, &it.ChangeBefore, &it.ChangeAfter, &it.IpAddress, &it.IsSuccess, &it.ErrorMsg, &it.UserAgent, &it.SessionID, &it.UserUUID, &it.DfUsrID, &it.Email, &it.IsActive, ); err != nil { return ActivityLogResult{}, err } if started.Valid { it.RequestStartedAt = &started.Time } if finished.Valid { it.RequestFinishedAt = &finished.Time } if dur.Valid { v := int(dur.Int64) it.DurationMs = &v } if status.Valid { v := int(status.Int64) it.HttpStatus = &v } items = append(items, it) } return ActivityLogResult{ Total: total, Items: items, }, nil }