├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── btc_rate.rb │ ├── network_fee.rb │ ├── multipay_group.rb │ ├── news_post.rb │ ├── category.rb │ ├── message_ref.rb │ ├── location.rb │ ├── message.rb │ ├── unitprice.rb │ ├── ticket_message.rb │ ├── bond.rb │ ├── admin_user.rb │ ├── ticket.rb │ ├── feedback.rb │ ├── payment_method.rb │ ├── shippingoption.rb │ ├── order_payout.rb │ ├── btc_address.rb │ └── generated_address.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── trademed.png │ │ ├── happy_face1.gif │ │ ├── placeholder.jpg │ │ ├── sad_face1.gif │ │ ├── neutral_face1.gif │ │ ├── state_transition_diagram.jpg │ │ ├── litecoin.svg │ │ ├── checkmark.svg │ │ └── bitcoin.svg │ ├── stylesheets │ │ ├── admin.css │ │ ├── adminarea.css.scss │ │ └── application.css │ └── javascripts │ │ └── application.js ├── controllers │ ├── concerns │ │ ├── .keep │ │ └── two_factor_auth.rb │ ├── admin │ │ ├── products_controller.rb │ │ ├── generated_address_controller.rb │ │ ├── btc_address_controller.rb │ │ ├── order_payouts_controller.rb │ │ ├── sessions_controller.rb │ │ ├── news_posts_controller.rb │ │ ├── btc_address_api_controller.rb │ │ ├── categories_controller.rb │ │ ├── locations_controller.rb │ │ ├── payouts_controller.rb │ │ ├── users_controller.rb │ │ └── messages_controller.rb │ ├── pages_controller.rb │ ├── products_controller.rb │ ├── shippingoptions_controller.rb │ ├── tickets_controller.rb │ └── vendor │ │ └── order_payouts_controller.rb ├── jobs │ ├── application_job.rb │ ├── count_payouts_job.rb │ ├── weekly_order_count_report_job.rb │ ├── autofinalize_job.rb │ ├── delete_order_postal_address_job.rb │ ├── btc_rates_blockchaininfo_job.rb │ ├── delete_order_payout_details_job.rb │ ├── btc_rates_bitpay_job.rb │ ├── update_orders_count_job.rb │ ├── ltc_rates_coinmarketcap_job.rb │ ├── update_orders_from_blockchain_job.rb │ ├── update_market_order_payouts_job.rb │ └── payout_confirmations_job.rb ├── views │ ├── feedbacks │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── show.html.erb │ │ ├── respond.html.erb │ │ └── _form.html.erb │ ├── admin │ │ ├── categories │ │ │ ├── edit.html.erb │ │ │ ├── new.html.erb │ │ │ ├── _form.html.erb │ │ │ └── index.html.erb │ │ ├── locations │ │ │ ├── edit.html.erb │ │ │ ├── new.html.erb │ │ │ ├── _form.html.erb │ │ │ └── index.html.erb │ │ ├── news_posts │ │ │ ├── edit.html.erb │ │ │ ├── new.html.erb │ │ │ ├── _form.html.erb │ │ │ └── index.html.erb │ │ ├── messages │ │ │ ├── show_conversation.html.erb │ │ │ ├── _deleted_messages.html.erb │ │ │ ├── _conversation.html.erb │ │ │ └── conversations.html.erb │ │ ├── sessions │ │ │ └── new.html.erb │ │ ├── tickets │ │ │ ├── index.html.erb │ │ │ └── new.html.erb │ │ ├── order_payouts │ │ │ └── edit.html.erb │ │ ├── users │ │ │ └── index.html.erb │ │ ├── products │ │ │ └── index.html.erb │ │ ├── btc_address │ │ │ └── search_form.html.erb │ │ └── generated_address │ │ │ └── search_form.html.erb │ ├── vendor │ │ ├── products │ │ │ ├── edit.html.erb │ │ │ ├── new.html.erb │ │ │ ├── index.html.erb │ │ │ └── show.html.erb │ │ ├── orders │ │ │ └── index.csv.erb │ │ └── order_payouts │ │ │ ├── index.html.erb │ │ │ └── _table.html.erb │ ├── shippingoptions │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _form.html.erb │ │ ├── index.html.erb │ │ └── show.html.erb │ ├── kaminari │ │ ├── _gap.html.erb │ │ ├── _first_page.html.erb │ │ ├── _last_page.html.erb │ │ ├── _next_page.html.erb │ │ ├── _prev_page.html.erb │ │ ├── _page.html.erb │ │ └── _paginator.html.erb │ ├── products │ │ ├── index.html.erb │ │ ├── _shippingoptions.html.erb │ │ ├── show.html.erb │ │ ├── _unitprices.html.erb │ │ └── _indextable.html.erb │ ├── application │ │ ├── _form_errors.html.erb │ │ ├── _categories.html.erb │ │ └── _admin_nav.html.erb │ ├── pages │ │ ├── vendor_directory.html.erb │ │ ├── news.html.erb │ │ ├── publickey.html.erb │ │ └── index.html.erb │ ├── orders │ │ ├── finalize_confirm.html.erb │ │ └── multipay.html.erb │ ├── users │ │ ├── account.html.erb │ │ ├── editpass.html.erb │ │ └── pgp_2fa.html.erb │ ├── sessions │ │ ├── new.html.erb │ │ └── new_pgp_2fa.html.erb │ ├── tickets │ │ ├── new.html.erb │ │ └── index.html.erb │ ├── messages │ │ ├── show_conversation.html.erb │ │ ├── _conversation.html.erb │ │ └── conversations.html.erb │ └── layouts │ │ └── application.html.erb └── helpers │ └── application_helper.rb ├── lib ├── assets │ └── .keep ├── tasks │ └── .keep ├── not_authorized.rb ├── address_pool_empty.rb ├── order_currency_conversion_failure.rb ├── paperclip_processors │ └── stripexif.rb ├── gpgkeyinfo.rb ├── gpg_operations.rb └── bitcoin_rpc.rb ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ └── .keep ├── controllers │ └── .keep ├── fixtures │ └── .keep ├── integration │ └── .keep └── test_helper.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── .rspec ├── config ├── initializers │ ├── script_log.rb │ ├── timeformats.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── litecoinrpc.rb │ ├── website_gpg_key.rb │ ├── backtrace_silencers.rb │ ├── bitcoinrpc.rb │ ├── wrap_parameters.rb │ ├── assets.rb │ ├── inflections.rb │ └── content_security_policy.rb ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── locales │ ├── en.yml │ └── humanizer.en.yml ├── storage.yml ├── secrets.yml ├── environments │ ├── development.rb │ └── test.rb └── puma.rb ├── bin ├── rake ├── bundle ├── rails ├── yarn ├── spring ├── update └── setup ├── examples ├── Screenshot_admin_users.png ├── Screenshot_product_index.png ├── Screenshot_vendor_message.png ├── Screenshot_account_created.png ├── Screenshot_admin_locations.png ├── Screenshot_generate_address.png ├── Screenshot_order_process_1.png ├── Screenshot_order_process_2.png ├── Screenshot_order_process_3.png ├── Screenshot_order_process_4.png ├── Screenshot_upload_to_market.png ├── Screenshot_check_watches_setup.png ├── Screenshot_vendor_orders_index.png ├── Screenshot_generate_address_test.png ├── Screenshot_vendor_message_index.png ├── Screenshot_admin_market_addresses.png ├── Screenshot_vendor_order_view_paid.png ├── Screenshot_vendor_order_view_pending.png ├── update_orders_from_blockchain_job.sh ├── count_payouts.sh ├── environment_example_payout.sh ├── update_btcRates_tor_example.rb └── environment_example_market.sh ├── .dockerignore ├── config.ru ├── Rakefile ├── imagemagick-policy.xml ├── spec ├── support │ └── utilities.rb └── rails_helper.rb ├── .gitignore ├── db └── seeds.rb ├── LICENSE ├── docker-compose.yml ├── Gemfile ├── Dockerfile ├── check_bitcoind_watches_setup.rb └── generate_address.rb /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/admin.css: -------------------------------------------------------------------------------- 1 | //= require adminarea 2 | -------------------------------------------------------------------------------- /lib/not_authorized.rb: -------------------------------------------------------------------------------- 1 | class NotAuthorized < StandardError 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /lib/address_pool_empty.rb: -------------------------------------------------------------------------------- 1 | class AddressPoolEmpty < StandardError 2 | end 3 | -------------------------------------------------------------------------------- /config/initializers/script_log.rb: -------------------------------------------------------------------------------- 1 | ::ScriptLog = Logger.new('log/script.log') 2 | -------------------------------------------------------------------------------- /app/views/feedbacks/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit feedback

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/admin/categories/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit category

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/admin/categories/new.html.erb: -------------------------------------------------------------------------------- 1 |

Create category

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/admin/locations/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit location

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/admin/locations/new.html.erb: -------------------------------------------------------------------------------- 1 |

Create location

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/admin/news_posts/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit News Post

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/vendor/products/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Editing Product

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/admin/news_posts/new.html.erb: -------------------------------------------------------------------------------- 1 |

Create News Post

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/shippingoptions/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Edit shipping option

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/shippingoptions/new.html.erb: -------------------------------------------------------------------------------- 1 |

Create shipping option

2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /lib/order_currency_conversion_failure.rb: -------------------------------------------------------------------------------- 1 | class OrderCurrencyConversionFailure < StandardError 2 | end 3 | -------------------------------------------------------------------------------- /app/assets/images/trademed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/app/assets/images/trademed.png -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /app/assets/images/happy_face1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/app/assets/images/happy_face1.gif -------------------------------------------------------------------------------- /app/assets/images/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/app/assets/images/placeholder.jpg -------------------------------------------------------------------------------- /app/assets/images/sad_face1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/app/assets/images/sad_face1.gif -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/images/neutral_face1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/app/assets/images/neutral_face1.gif -------------------------------------------------------------------------------- /config/initializers/timeformats.rb: -------------------------------------------------------------------------------- 1 | Time::DATE_FORMATS[:FHM] = '%F %H:%M %Z' 2 | Time::DATE_FORMATS[:FHMnozone] = '%F %H:%M' 3 | -------------------------------------------------------------------------------- /examples/Screenshot_admin_users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_admin_users.png -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= content_tag :a, raw(t 'views.pagination.truncate') %> 3 |
  • 4 | -------------------------------------------------------------------------------- /examples/Screenshot_product_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_product_index.png -------------------------------------------------------------------------------- /examples/Screenshot_vendor_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_vendor_message.png -------------------------------------------------------------------------------- /app/views/products/index.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <%= render 'indextable' %> 4 |
    5 |
    6 | -------------------------------------------------------------------------------- /examples/Screenshot_account_created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_account_created.png -------------------------------------------------------------------------------- /examples/Screenshot_admin_locations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_admin_locations.png -------------------------------------------------------------------------------- /examples/Screenshot_generate_address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_generate_address.png -------------------------------------------------------------------------------- /examples/Screenshot_order_process_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_order_process_1.png -------------------------------------------------------------------------------- /examples/Screenshot_order_process_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_order_process_2.png -------------------------------------------------------------------------------- /examples/Screenshot_order_process_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_order_process_3.png -------------------------------------------------------------------------------- /examples/Screenshot_order_process_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_order_process_4.png -------------------------------------------------------------------------------- /examples/Screenshot_upload_to_market.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_upload_to_market.png -------------------------------------------------------------------------------- /examples/Screenshot_check_watches_setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_check_watches_setup.png -------------------------------------------------------------------------------- /examples/Screenshot_vendor_orders_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_vendor_orders_index.png -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /examples/Screenshot_generate_address_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_generate_address_test.png -------------------------------------------------------------------------------- /examples/Screenshot_vendor_message_index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_vendor_message_index.png -------------------------------------------------------------------------------- /app/assets/images/state_transition_diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/app/assets/images/state_transition_diagram.jpg -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /examples/Screenshot_admin_market_addresses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_admin_market_addresses.png -------------------------------------------------------------------------------- /examples/Screenshot_vendor_order_view_paid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_vendor_order_view_paid.png -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /examples/Screenshot_vendor_order_view_pending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/B0bbyB0livia/trademed/HEAD/examples/Screenshot_vendor_order_view_pending.png -------------------------------------------------------------------------------- /app/views/kaminari/_first_page.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote %> 3 |
  • 4 | -------------------------------------------------------------------------------- /app/views/kaminari/_last_page.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} %> 3 |
  • 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | log/*log 2 | tmp/* 3 | Dockerfile 4 | .gitignore 5 | .dockerignore 6 | public/assets/* 7 | public/system/* 8 | *.swp 9 | .git 10 | spec/* 11 | test/* 12 | -------------------------------------------------------------------------------- /app/models/btc_rate.rb: -------------------------------------------------------------------------------- 1 | class BtcRate < ApplicationRecord 2 | validates :code, :inclusion => { in: Rails.configuration.currencies } 3 | belongs_to :payment_method 4 | end 5 | -------------------------------------------------------------------------------- /app/views/kaminari/_next_page.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote %> 3 |
  • 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /app/views/kaminari/_prev_page.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote %> 3 |
  • 4 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_trademed_session' 4 | -------------------------------------------------------------------------------- /app/models/network_fee.rb: -------------------------------------------------------------------------------- 1 | # All this does is display a number based on current week in the nav bar of vendors. 2 | class NetworkFee < ApplicationRecord 3 | validates :weeknum, uniqueness: true 4 | end 5 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/adminarea.css.scss: -------------------------------------------------------------------------------- 1 | // styles for admin area. 2 | // application layout will include this whenever an admin controller is handling request. 3 | .green_text { 4 | color: green; 5 | } 6 | .orange_text { 7 | color: orange; 8 | } 9 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: trademed_production 11 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /app/views/application/_form_errors.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <% if what.errors.any? %> 3 | <% what.errors.full_messages.each do |msg| %> 4 | <%= content_tag :div, msg, :class => "alert alert-danger" %> 5 | <% end %> 6 | <% end %> 7 |
    8 | -------------------------------------------------------------------------------- /app/models/multipay_group.rb: -------------------------------------------------------------------------------- 1 | class MultipayGroup < ApplicationRecord 2 | has_many :orders 3 | # This allows multipaygroupInstance.primary_order for setting and getting. 4 | belongs_to :primary_order, foreign_key: :primary_order_id , class_name: 'Order' 5 | 6 | end 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/models/news_post.rb: -------------------------------------------------------------------------------- 1 | class NewsPost < ApplicationRecord 2 | scope :sort_by_post_date, -> { order('post_date DESC') } 3 | validates :message, length: { maximum: 8000 } 4 | validates :message, format: { with: /\A[[[:print:]]\r\n]+\z/, 5 | message: "unexpected characters" } 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /examples/update_orders_from_blockchain_job.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # crontab entry example: 4 | # */5 * * * * SCRIPT=/path/to/script/update_orders_from_blockchain_job.sh; flock -n $SCRIPT.lock $SCRIPT 5 | 6 | docker-compose run --rm trademed rails r 'UpdateOrdersFromBlockchainJob.perform_now' 7 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /app/jobs/count_payouts_job.rb: -------------------------------------------------------------------------------- 1 | class CountPayoutsJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform 5 | # Not sure if possible to access return code on shell so will just look at output instead. 6 | count = Payout.where(paid: false).count 7 | puts count 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/feedbacks/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Feedback

    2 | 3 |

    4 | After leaving feedback you cannot change the rating but the feedback message can be changed. 5 | Therefore, in disputes, wait a week or two before leaving feedback because the issue may take time to resolve. 6 |

    7 | 8 | <%= render 'form' %> 9 | -------------------------------------------------------------------------------- /app/views/feedbacks/show.html.erb: -------------------------------------------------------------------------------- 1 |

    2 | Option: 3 | <%= @feedback.option %> 4 |

    5 | 6 |

    7 | Feedback: 8 | <%= @feedback.feedback %> 9 |

    10 | 11 | <%= link_to 'Edit', edit_feedback_path(@feedback) %> | 12 | <%= link_to 'Back', feedbacks_path %> 13 | -------------------------------------------------------------------------------- /app/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ApplicationRecord 2 | has_many :products 3 | validates :name, length: { minimum: 1, maximum: 255 } 4 | validates :name, format: { with: /\A[[:print:]]*\z/, message: "unexpected characters" } 5 | validates :name, uniqueness: true 6 | default_scope { order(:name) } 7 | end 8 | -------------------------------------------------------------------------------- /app/views/vendor/products/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Create product

    2 | 3 | <% if current_user.shippingoptions.count == 0 %> 4 |

    5 | Before adding a product, you need to create at least one <%= link_to 'shipping option', shippingoptions_path %> 6 |

    7 | <% else %> 8 | <%= render 'form' %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/kaminari/_page.html.erb: -------------------------------------------------------------------------------- 1 | <% if page.current? %> 2 |
  • 3 | <%= content_tag :a, page, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)) %> 4 |
  • 5 | <% else %> 6 |
  • 7 | <%= link_to page, url, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)) %> 8 |
  • 9 | <% end %> 10 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/litecoinrpc.rb: -------------------------------------------------------------------------------- 1 | # In development/testing there are two local bitcoind available for different purposes. 2 | # These are global instance objects used for making requests to the appropriate bitcoind. 3 | ::Payout_litecoinrpc = BitcoinRPC.new(Rails.configuration.payout_litecoinrpc_uri) 4 | ::Market_litecoinrpc = BitcoinRPC.new(Rails.configuration.market_litecoinrpc_uri) 5 | -------------------------------------------------------------------------------- /config/initializers/website_gpg_key.rb: -------------------------------------------------------------------------------- 1 | ::WebsiteGpgKey = Gpgkeyinfo.new(Rails.configuration.gpg_key_id) 2 | 3 | # Rails5 couldn't initialize a GpgOperations object from app/controllers/concerns/two_factor_auth.rb 4 | # without setting Rails.application.config.enable_dependency_loading true. This forces a load of lib/gpg_operations.rb 5 | # at boot to avoid that problem. 6 | GpgOperations 7 | -------------------------------------------------------------------------------- /app/views/pages/vendor_directory.html.erb: -------------------------------------------------------------------------------- 1 |

    Vendor directory

    2 | 3 |
    4 |

    5 |

    6 | 7 | 14 | 15 |
    16 |

    17 |
    18 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/models/message_ref.rb: -------------------------------------------------------------------------------- 1 | class MessageRef < ApplicationRecord 2 | belongs_to :user 3 | belongs_to :otherparty, foreign_key: :otherparty_id , class_name: 'User' 4 | belongs_to :message 5 | 6 | # default_scope breaks ActiveRecord .group() 7 | #default_scope { order(:created_at) } 8 | scope :unseen, -> { where("unseen = 1")} 9 | 10 | validates :direction, inclusion: { in: %w(sent received ) } 11 | validates :unseen, inclusion: { in: [0, 1] } 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/admin/products_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::ProductsController < ApplicationController 2 | before_action :require_admin 3 | 4 | def index 5 | @products = Product.where(deleted: false).order('created_at DESC') 6 | if params[:filter_vendor] 7 | @products = @products.joins(:vendor).where(users: {displayname: params[:filter_vendor]}) 8 | end 9 | @products = @products.page(params[:page]) 10 | @products_count = @products.count 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/jobs/weekly_order_count_report_job.rb: -------------------------------------------------------------------------------- 1 | # Show historical count of paid orders each week. 2 | class WeeklyOrderCountReportJob < ApplicationJob 3 | queue_as :default 4 | 5 | def perform 6 | Order.after_paid.select('COUNT(1) AS sum, EXTRACT(WEEK FROM created_at) AS week, EXTRACT(YEAR FROM created_at) AS year'). 7 | group('week, year'). 8 | order('year, week').each do |w| 9 | puts "#{w.year.to_i}/#{w.week.to_i} : #{w.sum}" 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/location.rb: -------------------------------------------------------------------------------- 1 | class Location < ApplicationRecord 2 | validates :description, length: { minimum: 2, maximum: 40 } 3 | validates :description, uniqueness: true 4 | default_scope { order(:description) } 5 | # These are ship-to locations. location.products returns all products that ship to this location. 6 | has_and_belongs_to_many :products 7 | 8 | def allow_destroy? 9 | Product.where(from_location: self).count == 0 && self.products.count == 0 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/bitcoinrpc.rb: -------------------------------------------------------------------------------- 1 | # In development/testing there are two local bitcoind available for different purposes. 2 | # These are global instance objects used for making requests to the appropriate bitcoind. 3 | ::Payout_bitcoinrpc = BitcoinRPC.new(Rails.configuration.payout_bitcoinrpc_uri) 4 | ::Market_bitcoinrpc = BitcoinRPC.new(Rails.configuration.market_bitcoinrpc_uri) 5 | # These are used as RPC arguments for calls that traditionally dealt with account names. Accounts were deprecated in 0.17. 6 | ::DummyStar = '*' 7 | ::DummyBlank = '' 8 | -------------------------------------------------------------------------------- /app/models/message.rb: -------------------------------------------------------------------------------- 1 | class Message < ApplicationRecord 2 | belongs_to :sender, foreign_key: :sender_id , class_name: 'User' 3 | belongs_to :recipient, foreign_key: :recipient_id , class_name: 'User' 4 | 5 | # User model describes this. 6 | has_many :message_refs 7 | 8 | default_scope { order(:created_at) } 9 | 10 | validates :body, presence: true 11 | validates :body, length: { maximum: 10000 , message: "length of message"} 12 | validates :body, format: { with: /\A[[[:print:]]\r\n]*\z/, 13 | message: "characters unexpected" } 14 | 15 | end 16 | -------------------------------------------------------------------------------- /app/views/pages/news.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    News archive

    4 | 5 | <% NewsPost.sort_by_post_date.each do |post| %> 6 | <%# nil.try always returns nil. in_time_zone(nil) gives UTC. so when visitor not logged in they get UTC time. %> 7 | 8 | 9 | 10 | 11 | <%end%> 12 |
    <%=post.post_date.in_time_zone(current_user.try(:timezone)).strftime('%F') %><%=simple_format h(post.message)%>
    13 | 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /app/views/feedbacks/respond.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: 'form_errors', locals: { what: @feedback } %> 2 | 3 |

    Respond to feedback

    4 | 5 |
    6 |
    7 | 8 | <%= form_for(@feedback, method: 'post', url: { action: 'save_response' }) do |f| %> 9 |
    10 | <%= f.label :response %> 11 | <%= f.text_area :response, class: 'form-control' %> 12 |
    13 |
    14 | <%= f.submit 'Submit response', class: 'btn btn-primary' %> 15 |
    16 | <% end %> 17 | 18 |
    19 |
    20 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast 4 | # It gets overwritten when you run the `spring binstub` command 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m) 11 | ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) 12 | ENV["GEM_HOME"] = "" 13 | Gem.paths = ENV 14 | 15 | gem "spring", match[1] 16 | require "spring/binstub" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /imagemagick-policy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/support/utilities.rb: -------------------------------------------------------------------------------- 1 | def sign_in(user) 2 | visit new_session_path 3 | fill_in 'Username' , with: user.username 4 | fill_in 'Password' , with: user.password 5 | click_button 'Login' 6 | end 7 | 8 | def admin_sign_in(user) 9 | visit admin_new_session_path 10 | fill_in 'Username' , with: user.username 11 | fill_in 'Password' , with: user.password 12 | click_button 'Login' 13 | end 14 | 15 | def in_browser(name) 16 | old_session = Capybara.session_name 17 | 18 | Capybara.session_name = name 19 | yield 20 | 21 | Capybara.session_name = old_session 22 | end 23 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.erb: -------------------------------------------------------------------------------- 1 | <%= paginator.render do -%> 2 | 15 | <% end -%> 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require application_layout 12 | */ 13 | -------------------------------------------------------------------------------- /app/views/admin/locations/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for([:admin, @location] , :html => { class: 'form-horizontal' }) do |f| %> 2 | 3 | <%= render partial: 'form_errors', locals: { what: @location } %> 4 | 5 |
    6 | <%= f.label :description, class: 'col-sm-2 control-label' %> 7 |
    8 | <%= f.text_field :description, class: 'form-control' %> 9 |
    10 |
    11 | 12 |
    13 |
    14 | <%= f.submit class: 'btn btn-primary' %> 15 |
    16 |
    17 | 18 | <% end %> 19 | -------------------------------------------------------------------------------- /app/views/admin/news_posts/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for([:admin, @news_post] , :html => { class: 'form-horizontal' }) do |f| %> 2 | 3 | <%= render partial: 'form_errors', locals: { what: @news_post } %> 4 | 5 |
    6 | <%= f.label :message, class: 'col-sm-2 control-label' %> 7 |
    8 | <%= f.text_area :message, rows: 10, class: 'form-control' %> 9 |
    10 |
    11 | 12 |
    13 |
    14 | <%= f.submit class: 'btn btn-primary' %> 15 |
    16 |
    17 | 18 | <% end %> 19 | -------------------------------------------------------------------------------- /examples/count_payouts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # example script that can be called from cron - count_payouts.sh >/dev/null 2>&1 4 | 5 | # cd to location of docker-compose.yml 6 | cd /home/rails/docker/ 7 | 8 | # docker-compose outputs CR character which breaks bash. 9 | # the "Removing ..." text is shown on stderr. 10 | # -T parameter required when running under cron otherwith no stdout produced. 11 | declare -i RESULT=$(/usr/local/bin/docker-compose run -T --rm trademed rails r 'CountPayoutsJob.perform_now' | tr -d '\015') 12 | if [ $RESULT -gt 0 ]; then 13 | mail -s "new payout $RESULT" email@example.com < /dev/null 14 | fi 15 | -------------------------------------------------------------------------------- /app/controllers/admin/generated_address_controller.rb: -------------------------------------------------------------------------------- 1 | # This controller is used on the payout server. 2 | class Admin::GeneratedAddressController < ApplicationController 3 | before_action :require_admin 4 | 5 | def search_form 6 | end 7 | 8 | # Searches bitcoin and litecoin addresses (any address in the table). 9 | def search 10 | address = params['btc_address'] 11 | @btc_address = GeneratedAddress.find_by_btc_address address 12 | if @btc_address 13 | render :search_form 14 | else 15 | redirect_to admin_generated_address_search_form_path, alert: 'Address not found' 16 | end 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /app/jobs/autofinalize_job.rb: -------------------------------------------------------------------------------- 1 | class AutofinalizeJob < ApplicationJob 2 | queue_as :default 3 | 4 | def perform() 5 | orders = Order.where('finalize_at < ?', Time.now).where(status: Order::SHIPPED) 6 | 7 | ScriptLog.info("#{self.class} - found #{orders.size} orders to finalize") 8 | 9 | # Callback in model will set vendor_payout_amount because status is changing. 10 | # Note, finalized_at is only set on FINALIZED orders, not AUTO_FINALIZED. 11 | orders.each do |order| 12 | order.update!(status: Order::AUTO_FINALIZED) 13 | ScriptLog.info("order_id: #{order.id} now AUTO_FINALIZED.") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/views/orders/finalize_confirm.html.erb: -------------------------------------------------------------------------------- 1 |

    Confirm finalize

    2 | 3 |

    4 | Order id: <%=@order.id%> 5 |

    6 |

    7 | Product: <%=@order.title%> 8 |

    9 |

    10 | Vendor: <%=@order.vendor.displayname%> 11 |

    12 | 13 |

    14 | This will pay the vendor 100% of funds paid. 15 |

    16 |

    17 | <%= form_for(@order, method: 'post', url: finalize_order_path(@order, return_to_index: true) ) do |f| %> 18 | <%= f.submit 'Finalize', class: 'btn btn-success form-button' %> 19 | <%= link_to 'Cancel', orders_path(), class: 'btn btn-default form-button' %> 20 | <% end %> 21 |

    22 | -------------------------------------------------------------------------------- /lib/paperclip_processors/stripexif.rb: -------------------------------------------------------------------------------- 1 | # http://stackoverflow.com/questions/28293289/how-to-create-custom-paperclip-processor-to-retrive-image-dimensions-rails-4 2 | module Paperclip 3 | class Stripexif < Processor 4 | def make 5 | basename = File.basename(file.path, File.extname(file.path)) 6 | dst_format = options[:format] ? ".\#{options[:format]}" : '' 7 | 8 | dst = Tempfile.new([basename, dst_format]) 9 | dst.binmode 10 | 11 | convert(':src -strip :dst', 12 | src: File.expand_path(file.path), 13 | dst: File.expand_path(dst.path)) 14 | 15 | dst 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/views/products/_shippingoptions.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% product.shippingoptions.each do |shipopt| %> 4 | 5 | 6 | 14 | 15 | 16 | <%end%> 17 |
    Shipping optionPrice
    <%= shipopt.description %> 7 | <% if current_user %> 8 | <%= to_users_currency(shipopt.currency, shipopt.price) %> <%= current_user.currency %> 9 | <%else%> 10 | <%# just display in whatever currency it was saved as %> 11 | <%= currency_format(shipopt.price) %> <%=shipopt.currency%> 12 | <% end %> 13 |
    18 | -------------------------------------------------------------------------------- /app/views/users/account.html.erb: -------------------------------------------------------------------------------- 1 |

    Account

    2 |
    3 |
    4 | 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /app/models/unitprice.rb: -------------------------------------------------------------------------------- 1 | class Unitprice < ApplicationRecord 2 | belongs_to :product, optional: true # product_id field may be nil. See orders controller. 3 | has_many :orders 4 | 5 | default_scope { order(:unit) } # to ensure unit prices displayed in ascending order. 6 | 7 | validates :unit, numericality: { greater_than: 0 } 8 | # Allow free products because vendor may give away samples where buyer only pays shipping. 9 | # When order is created the total cost must be greater than zero however. 10 | validates :price, numericality: { greater_than_or_equal_to: 0 } 11 | validates :currency, :inclusion => { in: Rails.configuration.currencies } 12 | end 13 | -------------------------------------------------------------------------------- /app/views/pages/publickey.html.erb: -------------------------------------------------------------------------------- 1 |

    Public key

    2 |

    3 | Please import this PGP key so you can authenticate this website in future. 4 |

    5 | Bitcoin payment addresses for orders will always be signed with this key. 6 |

    7 |

    8 | You should bookmark this site and always use the bookmark for future access to avoid phishing attacks. 9 |

    10 | 11 |

    12 | Key ID / fingerprint : 13 |

    14 | <%= WebsiteGpgKey.fingerprint %>
    15 | 
    16 | 17 | Key data for importing : 18 |
    19 | <%= WebsiteGpgKey.pubkey %>
    20 | 
    21 |

    22 | 23 | <% if flash.key?(:newuser) %> 24 | <%= link_to 'Continue', root_path, class: 'btn btn-primary' %> 25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/models/ticket_message.rb: -------------------------------------------------------------------------------- 1 | class TicketMessage < ApplicationRecord 2 | belongs_to :ticket 3 | default_scope { order('created_at DESC') } 4 | # We don't bother enforcing non-blank messages because these are removed by reject_if in Ticket model 5 | # so validation never done on TMs with blank messages. 6 | validates :message, length: { maximum: 10000 , message: "length of message"} 7 | validates :message, format: { with: /\A[[[:print:]]\r\n]*\z/, message: "characters unexpected" } 8 | validates :response, length: { maximum: 10000 , message: "length of message"} 9 | validates :response, format: { with: /\A[[[:print:]]\r\n]*\z/, 10 | message: "characters unexpected" } 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | Rails.application.config.assets.precompile += %w( admin.css ) 15 | -------------------------------------------------------------------------------- /app/views/admin/messages/show_conversation.html.erb: -------------------------------------------------------------------------------- 1 |

    Messages: <%= @user.displayname %> / <%=@otherparty.displayname %>

    2 | 3 |

    4 | Messages received by <%=@user.displayname%> are displayed brown color. This is similar to what the user will see 5 | except admin can see deleted messages appended with a delineation sentance between. 6 |

    7 | 8 |
    9 |
    10 | 11 | <%= render "conversation" %> 12 | <% if @deleted_messages.size > 0 %> 13 |

    Messages that have no references pointing to them (they were deleted by <%=@user.displayname%>) are shown below.

    14 | <%= render "deleted_messages" %> 15 | <%end%> 16 |
    17 |
    18 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require turbolinks 14 | //= require_tree . 15 | -------------------------------------------------------------------------------- /app/views/vendor/orders/index.csv.erb: -------------------------------------------------------------------------------- 1 | <% if @orders.size > 0 %> 2 | Creation time,Buyer,Product,Total quantity,Price in <%=current_user.currency%>,Pay price,Currency code,Exchange rate <%=current_user.currency%>,Order status 3 | <%end%> 4 | <% @orders.each do |order| %> 5 | <%= "%s,%s,%s,%g,%s,%s,%s,%s,%s" % [ 6 | order.created_at.in_time_zone(current_user.timezone).to_s(:FHMnozone), 7 | order.buyer.displayname, 8 | order.title, 9 | order.total_quantity, 10 | order.total_price_in_currency(current_user.currency).round(2), 11 | order.btc_price, 12 | order.payment_method.code, 13 | order.get_exchange_rate(session_user.currency).round(2), 14 | order.status] %> 15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | class PagesController < ApplicationController 2 | def index 3 | @products = Product.index_list 4 | @products = @products.page(params[:page]) 5 | # We only want news on front page, not when browsing through pages of products. 6 | if params[:page] 7 | @show_news = false 8 | else 9 | @show_news = true 10 | @news = NewsPost.where('post_date > ?', Time.now - 7.days).sort_by_post_date 11 | @show_news = false if @news.count == 0 12 | end 13 | if !is_market? 14 | redirect_to admin_path 15 | end 16 | end 17 | 18 | def vendor_directory 19 | @vendors = User.where(vendor: true).order(:displayname) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/vendor/order_payouts/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Payment history

    2 | 3 |

    Bitcoin payments processed

    4 | <%= render partial: 'table', locals: { order_payouts: @btc_order_payouts, summary: @btc_summary, currency: 'BTC' } %> 5 | 6 | <% if PaymentMethod.litecoin_exists? %> 7 |

    Litecoin payments processed

    8 | <%= render partial: 'table', locals: { order_payouts: @ltc_order_payouts, summary: @ltc_summary, currency: 'LTC' } %> 9 | <%end%> 10 |

    11 | Click order id to get more details of the truncated btc address and txid. If order has been deleted then it will not show here. 12 |

    13 |

    14 | Total sales revenue: <%= currency_format(@revenue) %> <%= session_user.currency %> 15 |

    16 | -------------------------------------------------------------------------------- /examples/environment_example_payout.sh: -------------------------------------------------------------------------------- 1 | source environment_example_market.sh 2 | export PAYOUT_BITCOIND_URI=http://bitcoinrpc:password@10.8.0.1:18332/ 3 | export PAYOUT_LITECOIND_URI=http://litecoinrpc:password@10.8.0.1:19332/ 4 | export MARKET_BITCOIND_URI= 5 | export MARKET_LITECOIND_URI= 6 | 7 | # These settings only used by payout server. 8 | export ADMIN_API_URI_BASE=http://admin.abcede1122334344.onion/ 9 | export TOR_PROXY_HOST=10.8.0.1 10 | export TOR_PROXY_PORT=9050 11 | export DATABASE_URL=postgres://trademed_prod:passwordxxxx@localhost/trademed_production 12 | export SECRET_KEY_BASE=exampleb48a032736bbb4fad2bd08a4be20ee4238fe34322a6b0d7fb4d41cd83def5910856995dad07c0fcbecd0f3b094a9530c036f8a4482bcd1bf6462ac7f1 13 | -------------------------------------------------------------------------------- /app/assets/images/litecoin.svg: -------------------------------------------------------------------------------- 1 | Litecoin 2 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /app/controllers/admin/btc_address_controller.rb: -------------------------------------------------------------------------------- 1 | # This controller is used on the market server. 2 | 3 | class Admin::BtcAddressController < ApplicationController 4 | before_action :require_admin 5 | 6 | def search_form 7 | end 8 | 9 | def search 10 | address = params['btc_address'] 11 | if Market_bitcoinrpc.validateaddress(address)["isvalid"] 12 | @btc_address = BtcAddress.find_by_address address 13 | if @btc_address 14 | render :search_form 15 | else 16 | redirect_to admin_btc_address_search_form_path, alert: 'Bitcoin address not found' 17 | end 18 | else 19 | redirect_to admin_btc_address_search_form_path, alert: 'Not a valid bitcoin address' 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /app/assets/images/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | !/log/.keep 13 | /tmp 14 | 15 | /public/assets/* 16 | # this dir is created by paperclip. 17 | /public/system/* 18 | # paperclip files saved to this dir during testing but directory should be automatically deleted anyway. 19 | /spec/test_files 20 | /vendor/bundle 21 | *.swp 22 | /app/assets/images/logo.* 23 | gpgkeyimport.txt 24 | .byebug_history 25 | -------------------------------------------------------------------------------- /app/controllers/products_controller.rb: -------------------------------------------------------------------------------- 1 | class ProductsController < ApplicationController 2 | before_action :set_product, except: [:index] 3 | 4 | def index 5 | category_id = params[:category_id] 6 | vendor_id = params[:vendor_id] 7 | if not category_id.nil? 8 | @products = Product.index_list.where(category_id: category_id) 9 | elsif not vendor_id.nil? 10 | @products = Product.index_list.where(vendor_id: vendor_id) 11 | else 12 | @products = Product.index_list 13 | end 14 | @products = @products.page(params[:page]) 15 | end 16 | 17 | def show 18 | end 19 | 20 | def buy 21 | @order = Order.new product_id: @product.id 22 | end 23 | 24 | private 25 | def set_product 26 | @product = Product.visible.find(params[:id]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/views/application/_categories.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    Browse categories
    3 |
    4 | <% categories = Product.listable.joins(:category).select("categories.id, name, count(products.id) as product_count").group("categories.id, name").order("name") %> 5 | 6 | 13 |
    14 | 15 |
    16 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Login

    2 | 3 | <%# sessions_path POST goes to sessions#create %> 4 | <%= form_tag sessions_path, class: 'form-horizontal' do %> 5 | 6 |
    7 | <%= label_tag :username, nil, class: 'col-sm-2 control-label' %> 8 |
    9 | <%= text_field_tag :username, params[:username], class: 'form-control' %> 10 |
    11 |
    12 | 13 |
    14 | <%= label_tag :password, nil, class: 'col-sm-2 control-label' %> 15 |
    16 | <%= password_field_tag :password, nil, class: 'form-control' %> 17 |
    18 |
    19 | 20 |
    21 |
    22 | <%= submit_tag "Login", id: 'submit', class: 'btn btn-primary' %> 23 |
    24 |
    25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/controllers/admin/order_payouts_controller.rb: -------------------------------------------------------------------------------- 1 | # This controller is used on the market server. 2 | # Decided not to do an admin index view of all OrderPayouts because this info is already displayed by the 3 | # Payout views on the payout server. 4 | class Admin::OrderPayoutsController < ApplicationController 5 | before_action :require_admin 6 | before_action :set_order_payout 7 | 8 | def edit 9 | end 10 | 11 | def update 12 | if @order_payout.update(order_payout_params) 13 | redirect_to admin_order_path(@order_payout.order), notice: 'Order payout updated' 14 | else 15 | render :edit 16 | end 17 | end 18 | 19 | private 20 | def set_order_payout 21 | @order_payout = OrderPayout.find(params[:id]) 22 | end 23 | 24 | def order_payout_params 25 | params.require(:order_payout).permit(:txid, :paid) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/views/admin/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

    Admin Login

    2 | 3 | <%# sessions_path POST goes to sessions#create %> 4 | <%= form_tag admin_sessions_path, class: 'form-horizontal' do %> 5 | 6 |
    7 | <%= label_tag :username, nil, class: 'col-sm-2 control-label' %> 8 |
    9 | <%= text_field_tag :username, params[:username], class: 'form-control' %> 10 |
    11 |
    12 | 13 |
    14 | <%= label_tag :password, nil, class: 'col-sm-2 control-label' %> 15 |
    16 | <%= password_field_tag :password, nil, class: 'form-control' %> 17 |
    18 |
    19 | 20 |
    21 |
    22 | <%= submit_tag "Login", id: 'submit', class: 'btn btn-default' %> 23 |
    24 |
    25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/shippingoptions/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@shippingoption, html: {class: 'form-horizontal'}) do |f| %> 2 | <%= render partial: 'form_errors', locals: { what: @shippingoption } %> 3 | 4 |
    5 | <%= f.label :description, class: 'col-sm-2 control-label' %> 6 |
    7 | <%= f.text_field :description, class: 'form-control' %> 8 |
    9 |
    10 |
    11 | <%= f.label :price, class: 'col-sm-2 control-label' %> 12 |
    13 | <%= f.text_field :price, class: 'form-control' %> 14 |

    Prices will be saved your preferred currency in account settings.

    15 |
    16 |
    17 |
    18 |
    19 | <%= f.submit class: 'btn btn-primary' %> 20 |
    21 |
    22 | <% end %> 23 | -------------------------------------------------------------------------------- /app/views/sessions/new_pgp_2fa.html.erb: -------------------------------------------------------------------------------- 1 |

    PGP Two factor authentication required

    2 | 3 | <% if @gpg_msg != nil %> 4 | 5 |

    6 | To copy, click text box and press 7 | ctrl a 8 | ctrl c 9 |

    10 |

    11 | 14 |

    15 | 16 | <%= form_tag(sessions_pgp_2fa_path, method: 'post', class: 'form-horizontal') do %> 17 |
    18 | <%= label_tag :secret_word, "Decrypted word", class: 'col-sm-2 control-label' %> 19 |
    20 | <%= text_field_tag :secret_word, nil, class: 'form-control' %> 21 |
    22 |
    23 | 24 |
    25 |
    26 | <%= submit_tag "Submit", id: 'submit', class: 'btn btn-primary' %> 27 |
    28 |
    29 | <%end%> 30 | 31 | <%end%> 32 | -------------------------------------------------------------------------------- /app/views/admin/categories/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for([:admin, @category] , :html => { class: 'form-horizontal' }) do |f| %> 2 | 3 | <%= render partial: 'form_errors', locals: { what: @category } %> 4 | 5 |
    6 | <%= f.label :name, class: 'col-sm-2 control-label' %> 7 |
    8 | <%= f.text_field :name, class: 'form-control' %> 9 |
    10 |
    11 | 12 |
    13 | <%= f.label :sortorder, 'Sort index', class: 'col-sm-2 control-label' %> 14 |
    15 | <%= f.text_field :sortorder, class: 'form-control' %> 16 |

    Currently this field is unused anywhere in the app.

    17 |
    18 |
    19 | 20 |
    21 |
    22 | <%= f.submit class: 'btn btn-primary' %> 23 |
    24 |
    25 | 26 | <% end %> 27 | -------------------------------------------------------------------------------- /app/controllers/concerns/two_factor_auth.rb: -------------------------------------------------------------------------------- 1 | module TwoFactorAuth 2 | extend ActiveSupport::Concern 3 | 4 | # Code shared by account settings and also during 2FA login. 5 | # Expects @user to be defined already by calling controller. 6 | def setup_pgp_2fa 7 | @gpg_msg = nil 8 | secret = SecureRandom.urlsafe_base64(nil, false) 9 | user_gpg = GpgOperations.new(@user.publickey) 10 | if !user_gpg.key_id.empty? 11 | # Function returns nil on any problems. 12 | @gpg_msg = user_gpg.encrypt("\n#{secret}\n\n") 13 | end 14 | if @gpg_msg 15 | # I think session data is encrypted but hash it anyway so no way to obtain secret from session data. 16 | session[:hash_of_secret] = Digest::SHA1.hexdigest(secret) 17 | end 18 | if @gpg_msg.nil? 19 | flash[:alert] = 'Unable to encrypt a message using your public key. Maybe the full key is not saved correctly.' 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/shippingoptions/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Shipping options

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | <% @shippingoptions.each do |shippingoption| %> 13 | 14 | 15 | 16 | 22 | 23 | <% end %> 24 | 25 |
    DescriptionPrice
    <%= shippingoption.description %><%= currency_format(shippingoption.price) %> <%=shippingoption.currency%> 17 |
    18 | <%= link_to 'Details', shippingoption_path(shippingoption), class: 'btn btn-primary btn-sm' %> 19 | <%= link_to 'Edit', edit_shippingoption_path(shippingoption), class: 'btn btn-primary btn-sm' %> 20 |
    21 |
    26 | 27 | <%= link_to 'Add shipping option', new_shippingoption_path, class: 'btn btn-primary' %> 28 | -------------------------------------------------------------------------------- /app/views/tickets/new.html.erb: -------------------------------------------------------------------------------- 1 |

    New ticket

    2 | 3 | <%= form_for(@ticket, :html => { class: 'form-horizontal' } ) do |f| %> 4 | 5 | <%= render partial: 'form_errors', locals: { what: @ticket} %> 6 | 7 |
    8 | <%= f.label :title, 'Title', class: 'col-sm-2 control-label' %> 9 |
    10 | <%= f.text_field :title, class: 'form-control' %> 11 |
    12 |
    13 | 14 | <%= f.fields_for :ticket_messages do |tm| %> 15 |
    16 | <%= tm.label :message, 'Message', class: 'col-sm-2 control-label' %> 17 |
    18 | <%= tm.text_area :message, rows: 5, class: 'form-control' %> 19 |
    20 |
    21 | <% end %> 22 | 23 |
    24 |
    25 | <%= f.submit class: 'btn btn-primary' %> 26 |
    27 |
    28 | 29 | <% end %> 30 | 31 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /app/views/pages/index.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <% if @show_news %> 4 |

    Recent news

    5 | 6 | <% @news.each do |post| %> 7 | <%# nil.try always returns nil. in_time_zone(nil) gives UTC. so when visitor not logged in they get UTC time. %> 8 | 9 | 10 | 11 | 12 | <%end%> 13 |
    <%=post.post_date.in_time_zone(current_user.try(:timezone)).strftime('%F') %><%=simple_format h(post.message)%>
    14 | <%end%> 15 | 16 |

    Product list

    17 | <% if Product.count == 0 %> 18 |

    19 | There are no products currently in the system. 20 | To create products, first register a vendor account. 21 |

    22 |

    23 | Create an admin account as described in INSTALL.md and using the admin account, create product categories and shipping locations. 24 |

    25 | <% end %> 26 | <%= render 'products/indextable' %> 27 |
    28 |
    29 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | 9 | 10 | bitcoin = PaymentMethod.create( name: 'Bitcoin', code: 'BTC' ) 11 | # It is not necessary to have an exchange rate, app will still start. Run one of the jobs to set rates asap. 12 | BtcRate.create(code: 'USD', rate: 500, payment_method: bitcoin ) 13 | 14 | # Example to initialize. The only effect of these records is to show on nav bar. 15 | #(1..53).each{|n| NetworkFee.create(weeknum: n) } 16 | 17 | #Category.create([ {name: 'Category1'},{name: 'Category2'} , {name: 'Category3'} ]) 18 | #Location.create([ { description: 'Russia' }, { description: 'USA' }, { description: 'World wide' }, { description: 'Canada' } ]) 19 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /app/models/bond.rb: -------------------------------------------------------------------------------- 1 | # To have a vendor account, admin can change their account to vendor after they have created an account. 2 | # Otherwise the standard way would be for them to use the user registration page and provide a valid "vendor code". 3 | # Vendor [authorization] codes are stored in the Bond relation and validity of the code depends on the bond attributes. 4 | 5 | # The registration process allows a vendor account to be created if the vendor code provided is the id of a bond record 6 | # and the bond record has no vendor assigned yet. 7 | 8 | # The bond record may refer to an order but it is not necessary for the bond to refer to an order. This is for situations where the site runs 9 | # with multiple vendors and new vendors must purchase a code to create a vendor account. 10 | # The admin can create a bond record simply to issue the vendor code to someone. 11 | class Bond < ApplicationRecord 12 | belongs_to :vendor, foreign_key: :vendor_id , class_name: 'User', optional: true 13 | belongs_to :order, optional: true 14 | end 15 | -------------------------------------------------------------------------------- /app/jobs/delete_order_postal_address_job.rb: -------------------------------------------------------------------------------- 1 | # Optionally run this job to delete the order address field on completed orders that are 8 weeks old. 2 | # Even though this field is a PGP message, for extra security delete it when no longer needed. 3 | 4 | def erase_address(order) 5 | ScriptLog.info("erased address on order id: #{order.id}") 6 | order.update!(address: '') 7 | end 8 | 9 | class DeleteOrderPostalAddressJob < ApplicationJob 10 | queue_as :default 11 | 12 | def perform 13 | ScriptLog.info("#{self.class} - erasing address field on old orders") 14 | 15 | Order.where(deleted_by_vendor: true).where(deleted_by_buyer: true).where.not(address: '').each do |o| 16 | erase_address(o) 17 | end 18 | 19 | Order.where('orders.created_at < ?', Time.now - 8.week). 20 | where(status: [Order::FINALIZED, Order::AUTO_FINALIZED, Order::DECLINED, Order::EXPIRED, Order::REFUND_FINALIZED, Order::ADMIN_FINALIZED] ). 21 | where.not(address: '').each do |o| 22 | erase_address(o) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /examples/update_btcRates_tor_example.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'net/https' 3 | require 'socksify/http' 4 | require 'json' 5 | 6 | # Example code only for requesting an SSL site over TOR. 7 | # Bitpay uses Cloudflare which blocks TOR so this won't work. 8 | 9 | uri = URI.parse('https://bitpay.com/api/rates') 10 | http = nil 11 | 12 | tor_proxy_host = Rails.configuration.tor_proxy_host 13 | tor_proxy_port = Rails.configuration.tor_proxy_port 14 | # If tor_proxy_host, tor_proxy_port are nil it silently falls back to not using a proxy to complete the request. 15 | http = Net::HTTP.SOCKSProxy(tor_proxy_host, tor_proxy_port).new(uri.host, uri.port) 16 | http.use_ssl = true 17 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 18 | 19 | response = http.get(uri) 20 | rates = JSON.parse(response.body) 21 | #=> [{"code"=>"USD", "name"=>"US Dollar", "rate"=>226.68}, 22 | # {"code"=>"EUR", "name"=>"Eurozone Euro", "rate"=>199.296444}, 23 | # {"code"=>"GBP", "name"=>"Pound Sterling", "rate"=>150.81276}, 24 | # {"code"=>"JPY", "name"=>"Japanese Yen", "rate"=>26806.1391}, ... 25 | -------------------------------------------------------------------------------- /app/views/admin/news_posts/index.html.erb: -------------------------------------------------------------------------------- 1 |

    News Posts

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <% @news_posts.each do |post| %> 14 | 15 | 16 | 17 | 26 | 27 | <%end%> 28 | 29 | 30 |
    DateMessage
    <%= post.post_date.in_time_zone(admin_user.timezone).to_s(:FHMnozone) %><%= simple_format h(post.message) %> 18 |
    19 | <%# Put the edit link inside form just for formatting so they are one same line %> 20 | <%= form_tag([:admin, post], method: :delete) do %> 21 | <%= link_to 'Edit', edit_admin_news_post_path(post), class: "btn btn-primary" %> 22 | <%= submit_tag("Delete", class: 'btn btn-danger') %> 23 | <% end %> 24 |
    25 |
    31 |

    <%= link_to 'Add News post', new_admin_news_post_path(), class: "btn btn-primary" %>

    32 | -------------------------------------------------------------------------------- /app/views/admin/messages/_deleted_messages.html.erb: -------------------------------------------------------------------------------- 1 | <%# sort hash by keys %> 2 | <% @deleted_message_daygroups.sort.reverse.each do |day, messages| %> 3 |
    4 |
    <%= day.in_time_zone(admin_user.timezone).strftime('%F') %>
    5 | <% messages.sort_by{|msg| msg.created_at}.reverse.each do |msg| %> 6 | <% cssclass = [] %> 7 | <% cssclass.push msg.sender == @user ? 'sent' : 'received' %> 8 | <% if msg.body[/-----BEGIN/] %> 9 | <% cssclass.push 'encrypted' %> 10 | <% else %> 11 | <% cssclass.push 'plaintext' %> 12 | <% end %> 13 |
    14 | <%= msg.body.split("\r\n").collect{ |line| h line}.join("
    ").html_safe %> 15 |
    16 | <%# The above message is printed on one long line in the source code with no newline chars. 17 | This allows copying PGP messages correctly while using the break-word styling in Firefox. 18 | Chrome doesn't have the copy problem that Firefox has. %> 19 | <% end %> 20 |
    <%# daygroup %> 21 | 22 | <% end %> 23 | -------------------------------------------------------------------------------- /app/jobs/btc_rates_blockchaininfo_job.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'json' 3 | 4 | # Exchange rates from blockchain.info. 5 | class BtcRatesBlockchaininfoJob < ApplicationJob 6 | queue_as :default 7 | 8 | def perform() 9 | ScriptLog.info("#{self.class} - updating bitcoin exchange rates") 10 | 11 | uri = URI('https://blockchain.info/ticker') 12 | response_string = Net::HTTP.get(uri) 13 | btc_rates = JSON.parse(response_string) 14 | 15 | # => { "USD" : {"15m" : 7227.2922081, "last" : 7227.2922081, "buy" : 7227.2922081, "sell" : 7227.2922081, "symbol" : "$"}, 16 | # "AUD" : {"15m" : 10020.300963796868, "last" : 10020.300963796868, "buy" : 10020.300963796868, "sell" : 10020.300963796868, "symbol" : "$"}, 17 | 18 | bitcoin_key = PaymentMethod.bitcoin.id 19 | Rails.configuration.currencies.each do |currency| 20 | raise unless btc_rates.has_key?(currency) 21 | btcrate = BtcRate.find_or_create_by!(code: currency, payment_method_id: bitcoin_key) 22 | btcrate.rate = btc_rates[currency]["15m"] 23 | btcrate.save! 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/assets/images/bitcoin.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a starting point to setup your application. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:setup' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /app/jobs/delete_order_payout_details_job.rb: -------------------------------------------------------------------------------- 1 | # On the market server, OrderPayout records store bitcoin txids about payments to vendors and buyers. 2 | # This info is mostly useful around time the payment is made but after a few weeks it is no longer of much interest. 3 | # Optionally run this job to delete bitcoin transaction details that occured three weeks ago so if database is exposed then less info about transactions is revealed. 4 | # This information is only visible to vendor on order view and the order_payouts index. 5 | # There are still records of the btc amount paid but not the txid or btc address. 6 | class DeleteOrderPayoutDetailsJob < ApplicationJob 7 | queue_as :default 8 | 9 | def perform 10 | OrderPayout.where('order_payouts.updated_at < ?', Time.now - 3.weeks).where(paid: true).update_all(txid: nil, btc_address: nil) 11 | # When an order is deleted by vendor, then no details of it show in order_payouts index so ok to purge these fields regardless of age. 12 | OrderPayout.joins(:order).where('orders.deleted_by_vendor': true).where(paid: true).update_all(txid: nil, btc_address: nil) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Trademed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/models/admin_user.rb: -------------------------------------------------------------------------------- 1 | class AdminUser < ApplicationRecord 2 | has_secure_password 3 | validates :password, length: { minimum: 8, maximum: 30 }, :if => :validate_password? 4 | validates :password_confirmation, length: { minimum: 8, maximum: 30 }, :if => :validate_password? 5 | 6 | validates :username, length: { minimum: 4, maximum: 20 } 7 | validates :username, uniqueness: true 8 | validates :username, format: { with: /\A[\w\d]+\z/, 9 | message: "permitted characters are alphabetic or numeric only" } 10 | validates :displayname, length: { minimum: 3, maximum: 30 } 11 | validates :displayname, uniqueness: true 12 | validates :displayname, format: { with: /\A[\w\d]+\z/, 13 | message: "permitted characters are alphabetic or numeric only" } 14 | validate :username_differs_displayname 15 | 16 | # http://guides.rubyonrails.org/active_record_validations.html#custom-methods 17 | def username_differs_displayname 18 | errors.add(:displayname, "Username must be different to Displayname") if username == displayname 19 | end 20 | 21 | def validate_password? 22 | password.present? || password_confirmation.present? 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/views/admin/locations/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Locations

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @locations.each do |loc| %> 15 | 16 | 17 | 18 | 19 | 28 | 29 | <%end%> 30 | 31 | 32 |
    NameProduct count (ship from)Product count (ship to)
    <%= loc.description %><%= Product.where(from_location: loc).count %><%= loc.products.count %> 20 |
    21 | <%# Put the edit link inside form just for formatting so they are one same line %> 22 | <%= form_tag([:admin, loc], method: :delete) do %> 23 | <%= link_to 'Edit', edit_admin_location_path(loc), class: "btn btn-primary btn-sm" %> 24 | <%= submit_tag("Delete", class: "btn btn-danger btn-sm") if loc.allow_destroy? %> 25 | <% end %> 26 |
    27 |
    33 |

    <%= link_to 'Add location', new_admin_location_path, class: "btn btn-primary" %>

    34 | -------------------------------------------------------------------------------- /app/jobs/btc_rates_bitpay_job.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'json' 3 | 4 | class BtcRatesBitpayJob < ApplicationJob 5 | queue_as :default 6 | 7 | def perform() 8 | ScriptLog.info("#{self.class} - updating bitcoin exchange rates") 9 | 10 | uri = URI('https://bitpay.com/api/rates') 11 | response_string = Net::HTTP.get(uri) 12 | btc_rates = JSON.parse(response_string) 13 | #=> [{"code"=>"USD", "name"=>"US Dollar", "rate"=>226.68}, 14 | # {"code"=>"EUR", "name"=>"Eurozone Euro", "rate"=>199.296444}, 15 | # {"code"=>"GBP", "name"=>"Pound Sterling", "rate"=>150.81276}, 16 | # {"code"=>"JPY", "name"=>"Japanese Yen", "rate"=>26806.1391}, ... 17 | 18 | bitcoin_key = PaymentMethod.bitcoin.id 19 | Rails.configuration.currencies.each do |currency| 20 | # currency_rate should be an array with one item. 21 | currency_rate = btc_rates.select { |rate| rate['code'] == currency } 22 | raise if currency_rate.empty? 23 | btc_rate = BtcRate.find_or_create_by!(code: currency, payment_method_id: bitcoin_key) 24 | btc_rate.rate = currency_rate[0]['rate'] 25 | btc_rate.save! 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/views/products/show.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <% if is_vendor? %> 3 |
    4 | Vendors can view but cannot make purchases. 5 | <% if @product.vendor == current_user %> 6 | <%= link_to 'Click this link for vendor product view', vendor_product_path(@product) %>. 7 | <%end%> 8 |
    9 | <% end %> 10 | 11 | <% if @product.allow_purchase? == false %> 12 | 13 | <%end%> 14 |
    15 | 16 | <%= render partial: 'product', object: @product %> 17 | 18 |

    19 | <% feedback_count = @product.vendor_feedbacks.count %> 20 | <%= link_to "Feedback (#{feedback_count})", feedbacks_path(product_id: @product.id), class: 'btn btn-default' %> 21 |

    22 | 23 |

    24 | <% if @product.allow_purchase? == false || is_vendor? %> 25 | <%= link_to 'Purchase', new_order_path(product: @product.id), class: 'btn btn-primary form-button disabled' %> 26 | <%else%> 27 | <%= link_to 'Purchase', new_order_path(product: @product.id), class: 'btn btn-primary form-button' %> 28 | <%end%> 29 |

    30 | -------------------------------------------------------------------------------- /app/controllers/admin/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::SessionsController < ApplicationController 2 | before_action :require_admin, only: [:destroy] 3 | 4 | # for login form. 5 | def new 6 | if current_user || admin_user 7 | flash.now[:alert] = 'You are currently logged in. Logout first before logging in again.' 8 | end 9 | end 10 | 11 | def create 12 | admin = AdminUser.find_by_username(params[:username]) 13 | if admin && admin.authenticate(params[:password]) 14 | session[:admin_id] = admin.id 15 | session[:admin] = true 16 | 17 | message = "Admin login success for #{admin.username}" 18 | redirect_to admin_path, :notice => message 19 | else 20 | sleep(Random.rand) 21 | # .now makes message available to current request, instead of next request. 22 | flash.now[:alert] = "Sorry, that username or password could not be found. Please try entering your username and password again." 23 | render "new" 24 | end 25 | end 26 | 27 | def destroy 28 | session[:admin_id] = nil 29 | session[:admin] = nil 30 | redirect_to admin_new_session_path, :notice => "You have now logged out" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/views/users/editpass.html.erb: -------------------------------------------------------------------------------- 1 |

    Change password

    2 | 3 | <%= form_for(@user, url: {action: "updatepass"}, html: {class: 'form-horizontal'}) do |f| %> 4 | <%= render partial: 'form_errors', locals: { what: @user } %> 5 | 6 |
    7 | <%= f.label :oldpassword, "Current password", class: 'col-sm-2 control-label' %> 8 |
    9 | <%= f.password_field :oldpassword, class: 'form-control' %> 10 |
    11 |
    12 | 13 |
    14 | <%= f.label :password, "New password", class: 'col-sm-2 control-label' %> 15 |
    16 | <%= f.password_field :password, class: 'form-control' %> 17 |
    18 |
    19 | 20 |
    21 | <%= f.label :password_confirmation, "Confirm password", class: 'col-sm-2 control-label' %> 22 |
    23 | <%= f.password_field :password_confirmation, class: 'form-control' %> 24 |
    25 |
    26 | 27 |
    28 |
    29 | <%= f.submit "Change", id: 'submit', class: 'btn btn-default' %> 30 |
    31 |
    32 | <% end %> 33 | -------------------------------------------------------------------------------- /app/models/ticket.rb: -------------------------------------------------------------------------------- 1 | class Ticket < ApplicationRecord 2 | belongs_to :user 3 | has_many :ticket_messages, dependent: :destroy 4 | 5 | # Ordering required by views, such as index, admin/show. 6 | #default_scope { order('created_at DESC, status DESC') } 7 | 8 | # User or admin may close the ticket without entering a new message, 9 | # in that case we update the Ticket but don't create a new TicketMessage (reject_if). 10 | # Positive check for nil means the parameter was not submitted. 11 | # Positive check for blank means submitted but empty. 12 | accepts_nested_attributes_for :ticket_messages, allow_destroy: false, reject_if: lambda { |attr| 13 | (attr['response'].nil? && attr['message'].blank?) || (attr['message'].nil? && attr['response'].blank?) 14 | } 15 | 16 | # Once created, don't allow changing this attribute. 17 | attr_readonly :title 18 | 19 | validates_associated :ticket_messages 20 | validates :status, :inclusion => { in: %w(open closed info) } 21 | validates :title, length: { minimum: 1, maximum: 255 } 22 | validates :title, format: { with: /\A[[:print:]]*\z/, 23 | message: "unexpected characters" } 24 | 25 | # Paginate page size. 26 | paginates_per 100 27 | end 28 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # policy.style_src :self, :https 14 | 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | 19 | # If you are using UJS then enable automatic nonce generation 20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 21 | 22 | # Report CSP violations to a specified URI 23 | # For further information see the following documentation: 24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 25 | # Rails.application.config.content_security_policy_report_only = true 26 | -------------------------------------------------------------------------------- /app/views/admin/messages/_conversation.html.erb: -------------------------------------------------------------------------------- 1 | <%# sort hash by keys %> 2 | <% @message_daygroups.sort.reverse.each do |day, message_refs| %> 3 |
    4 |
    <%= day.in_time_zone(admin_user.timezone).strftime('%F') %>
    5 | <% 6 | message_refs.sort_by{|msg| msg.created_at}.reverse.each do |message_ref| 7 | cssclass = [] 8 | cssclass.push message_ref.direction == 'sent' ? 'sent' : 'received' 9 | if message_ref.message.body[/-----BEGIN/] 10 | cssclass.push 'encrypted' 11 | else 12 | cssclass.push 'plaintext' 13 | end 14 | %> 15 |
    <%= message_ref.created_at.in_time_zone(admin_user.timezone).to_s(:FHM) %>
    16 |
    17 | <%= message_ref.message.body.split("\r\n").collect{ |line| h line}.join("
    ").html_safe %> 18 |
    19 | <%# The above message is printed on one long line in the source code with no newline chars. 20 | This allows copying PGP messages correctly while using the break-word styling in Firefox. 21 | Chrome doesn't have the copy problem that Firefox has. %> 22 | <% end %> 23 |
    <%# daygroup %> 24 | 25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/vendor/products/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Products

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% @products.each do |product| %> 16 | 17 | 18 | 19 | 24 | 29 | 35 | 36 | <% end %> 37 | 38 |
    TitleStock quantitySales enabledHidden
    <%= product.title %><%= sprintf("%g", product.stock) %> 20 | <% if product.available_for_sale %> 21 | <%=image_tag('checkmark.svg')%> 22 | <%end%> 23 | 25 | <% if product.hidden %> 26 | <%=image_tag('checkmark.svg')%> 27 | <%end%> 28 | 30 |
    31 | <%= link_to 'Details', vendor_product_path(product), class: "btn btn-primary btn-sm" %> 32 | <%= link_to 'Edit', edit_vendor_product_path(product), class: "btn btn-primary btn-sm" %> 33 |
    34 |
    39 | 40 |

    <%= link_to 'Add product', new_vendor_product_path, class: "btn btn-primary" %>

    41 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/views/admin/tickets/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Support tickets

    2 | 3 |

    To create a new ticket for a user, click their username link on users index page.

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @tickets.each do |ticket| %> 18 | 19 | 20 | 21 | 22 | 29 | 34 | 35 | <% end %> 36 | 37 |
    DateTitleDisplaynameStatus
    <%= ticket.created_at.in_time_zone(admin_user.timezone).to_s(:FHMnozone) %><%= ticket.title %><%= ticket.user.displayname %> 23 | <% labeltype = (ticket.status == 'closed') ? 'warning' : 'primary' %> 24 | <%= ticket.status %> 25 | <% if ticket.ticket_messages.where(message_seen: false).count > 0 %> 26 | unseen 27 | <%end%> 28 | 30 |
    31 | <%= link_to 'Details', admin_ticket_path(ticket), class: 'btn btn-primary' %> 32 |
    33 |
    38 | <%= paginate @tickets %> 39 | -------------------------------------------------------------------------------- /app/views/admin/tickets/new.html.erb: -------------------------------------------------------------------------------- 1 |

    New ticket

    2 | 3 | <%= form_for([:admin, @ticket], :html => { class: 'form-horizontal' } ) do |f| %> 4 | 5 | <%= render partial: 'form_errors', locals: { what: @ticket} %> 6 | 7 |
    8 | <%= f.label :title, 'Title', class: 'col-sm-2 control-label' %> 9 |
    10 | <%= f.text_field :title, class: 'form-control' %> 11 | <%= f.hidden_field :user_id %> 12 |
    13 |
    14 | 15 |
    16 | <%= f.label :status, class: 'col-sm-2 control-label' %> 17 |
    18 | <%= f.select(:status, %w(open info), {}, class: 'form-control') %> 19 |
    20 |
    21 | 22 | <%= f.fields_for :ticket_messages do |tm| %> 23 |
    24 | <%= tm.label :response, 'Message', class: 'col-sm-2 control-label' %> 25 |
    26 | <%= tm.text_area :response, rows: 5, class: 'form-control' %> 27 | <%= tm.hidden_field(:response_seen) %> 28 |
    29 |
    30 | <% end %> 31 | 32 |
    33 |
    34 | <%= f.submit class: 'btn btn-primary' %> 35 |
    36 |
    37 | 38 | <% end %> 39 | 40 | -------------------------------------------------------------------------------- /app/controllers/admin/news_posts_controller.rb: -------------------------------------------------------------------------------- 1 | # Used on market server. 2 | class Admin::NewsPostsController < ApplicationController 3 | before_action :require_admin 4 | before_action :set_news_post, only: [:edit, :update, :destroy] 5 | 6 | def index 7 | @news_posts = NewsPost.sort_by_post_date 8 | end 9 | 10 | def new 11 | @news_post = NewsPost.new 12 | end 13 | 14 | def edit 15 | end 16 | 17 | def create 18 | @news_post = NewsPost.new(news_post_params) 19 | @news_post.post_date = Time.now 20 | if @news_post.save 21 | redirect_to admin_news_posts_path 22 | else 23 | render action: 'new' 24 | end 25 | end 26 | 27 | def update 28 | if @news_post.update(news_post_params) 29 | redirect_to admin_news_posts_path, notice: "News post updated" 30 | else 31 | render :edit 32 | end 33 | end 34 | 35 | def destroy 36 | if @news_post.destroy 37 | redirect_to admin_news_posts_path, notice: "News post deleted" 38 | else 39 | redirect_to admin_news_posts_path, alert: "Error deleting news post" 40 | end 41 | end 42 | 43 | private 44 | def set_news_post 45 | @news_post = NewsPost.find(params[:id]) 46 | end 47 | 48 | def news_post_params 49 | params.require(:news_post).permit(:message) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/admin/btc_address_api_controller.rb: -------------------------------------------------------------------------------- 1 | # This controller is used on the market server. 2 | # All methods are API methods. 3 | 4 | class Admin::BtcAddressApiController < ApplicationController 5 | before_action :require_admin_api_key 6 | skip_before_action :verify_authenticity_token 7 | 8 | # Create new or update existing btc address pgp sig. 9 | # Do one per request so don't exceed any body size limits. 10 | # Return http 500 on failure. 11 | def import 12 | safe_params = params.require(:btc_address).permit(:address, :pgp_signature, :address_type) 13 | assign_params = safe_params.reject {|k,v| k == 'address_type'} 14 | btc_address = BtcAddress.find_by_address safe_params['address'] 15 | if btc_address 16 | # update address pgp sig unless already assigned to an order. 17 | btc_address.update!(assign_params) unless btc_address.order 18 | else 19 | btc_address = BtcAddress.new assign_params 20 | if safe_params['address_type'] == 'BTC' 21 | btc_address.payment_method = PaymentMethod.bitcoin 22 | elsif safe_params['address_type'] == 'LTC' 23 | btc_address.payment_method = PaymentMethod.litecoin 24 | else 25 | raise 26 | end 27 | btc_address.save! 28 | end 29 | render json: { api_return_code: 'success' } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/shippingoptions/show.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    3 |

    4 | Description 5 |
    6 |
    7 | <%= @shippingoption.description %> 8 |
    9 |

    10 |
    11 | 12 |
    13 |

    14 |

    15 | Price 16 |
    17 |
    18 | <%= currency_format @shippingoption.price %> <%=@shippingoption.currency%> 19 |
    20 |

    21 |
    22 | 23 |
    24 | <%= link_to 'Edit', edit_shippingoption_path(@shippingoption), class: 'btn btn-primary form-button' %> 25 | <%= link_to 'Delete', '#confirm_delete', class: 'btn btn-danger form-button' %> 26 |
    27 | 28 | 43 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: de210beea3f422d9442b24871f280bd1305b99ae4279ab3e65a23560000a07d25917b7ab2550373605468e91fd327418db98fb819e9b0c4b68dcf5db4d74c585 22 | 23 | test: 24 | secret_key_base: 51b299b29e05c77951990d303905ebf5144faeed845a483418797946ca6fde88a9c1f0cea5cc8a286e5d9e6a822d4d385d2a3c0a3f85de383437f24db9f14334 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /app/models/feedback.rb: -------------------------------------------------------------------------------- 1 | class Feedback < ApplicationRecord 2 | belongs_to :order 3 | belongs_to :placedby, foreign_key: :placedby_id , class_name: 'User' 4 | belongs_to :placedon, foreign_key: :placedon_id , class_name: 'User' 5 | validates :rating, inclusion: { in: %w(positive neutral negative)} 6 | validates :feedback, length: { minimum: 1, maximum: 1000 } 7 | validates :feedback, format: { with: /\A[[[:print:]]\r\n]*\z/, message: "characters unexpected" } 8 | validates :response, length: { minimum: 0, maximum: 1000 } 9 | validates :response, format: { with: /\A[[[:print:]]\r\n]*\z/, message: "characters unexpected" } 10 | validate :order_state_allows_feedback 11 | validate :check_authorization_to_create, on: :create 12 | 13 | scope :sortbynewest, -> { order('created_at DESC') } 14 | scope :positive, -> { where(rating: 'positive') } 15 | scope :neutral, -> { where(rating: 'neutral') } 16 | scope :negative, -> { where(rating: 'negative') } 17 | 18 | # Number of items per page. 19 | paginates_per 100 20 | 21 | protected 22 | def check_authorization_to_create 23 | errors.add(:order, "not authorized to create feedback on this order") unless (placedby == order.vendor || placedby == order.buyer) 24 | end 25 | 26 | def order_state_allows_feedback 27 | errors.add(:order, "order state does not allow feedback") unless order.allow_feedback_submission? 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/admin/categories_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::CategoriesController < ApplicationController 2 | before_action :require_admin 3 | before_action :set_category, only: [:show, :edit, :update, :destroy] 4 | 5 | def index 6 | @categories = Category.all 7 | end 8 | 9 | def new 10 | @category = Category.new 11 | end 12 | 13 | def create 14 | @category = Category.new(category_params) 15 | if @category.save 16 | redirect_to admin_categories_path, notice: 'Category was created' 17 | else 18 | render :new 19 | end 20 | end 21 | 22 | def update 23 | if @category.update(category_params) 24 | redirect_to admin_categories_path, notice: 'Category was updated' 25 | else 26 | render :edit 27 | end 28 | end 29 | 30 | def destroy 31 | # A product has a category. When category destroyed, product will refer to missing category_id. 32 | # Therefore prevent delete when category in use. 33 | if @category.products.count == 0 && @category.destroy 34 | redirect_to admin_categories_path, notice: 'Category deleted' 35 | else 36 | redirect_to admin_categories_path, alert: 'Category delete failed' 37 | end 38 | end 39 | 40 | private 41 | def set_category 42 | @category = Category.find(params[:id]) 43 | end 44 | 45 | def category_params 46 | params.require(:category).permit(:name, :sortorder) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/views/messages/show_conversation.html.erb: -------------------------------------------------------------------------------- 1 |

    Messages with <%= @message.recipient.displayname %>

    2 | 3 |

    <%= link_to 'show their public key', '#copy_users_pubkey_modal' %>

    4 | 5 |
    6 |
    7 | <%= form_for(@message) do |f| %> 8 | <%= render partial: 'form_errors', locals: { what: @message } %> 9 | 10 |
    11 | <%= f.label :body, "New message" %> 12 | <%= f.text_area :body, rows: 5, class: 'form-control' %> 13 |
    14 | 15 | <%= f.hidden_field :recipient_id %> 16 | 17 |
    18 | <%= f.submit "Send Message", id: 'submit', class: 'btn btn-primary' %> 19 |
    20 | <% end %> 21 | 22 | <%= render "conversation" %> 23 | 24 |
    25 |
    26 | 27 | 49 | -------------------------------------------------------------------------------- /app/jobs/update_orders_count_job.rb: -------------------------------------------------------------------------------- 1 | # Counter cache is enabled so products.orders_count variable is automatically incremented by rails every time an order created. 2 | # This variable is used for sorting most popular products. However it includes orders in states 'before confirmed' and 'payment pending' 3 | # so popularity influenced by creating lots of orders in those states. It seems like some buyers will create 'before confirmed' orders 4 | # as a way to get a bitcoin price estimate. 5 | # This job resets all products orders_count based on number of orders that were paid. 6 | # Only count orders in last 4 weeks because we want current popularity, not a product that was popular a year ago that no one is buying currently. 7 | # Rails prevents altering orders_count directly and the functions for adjusting it are not suitable for setting to arbitrary values, 8 | # therefore update using SQL. 9 | # A better solution would be to add another attribute to Product to track this info. 10 | class UpdateOrdersCountJob < ApplicationJob 11 | queue_as :default 12 | 13 | def perform 14 | ActiveRecord::Base.connection.execute("UPDATE products SET orders_count = 0;") 15 | 16 | Order.where('created_at > ?', Time.now - 4.week).after_paid. 17 | select('COUNT(1) AS cnt, product_id').group(:product_id).each do |p| 18 | ActiveRecord::Base.connection.execute("UPDATE products SET orders_count = #{p.cnt} WHERE id = '#{p.product_id}';") 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/admin/order_payouts/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Edit order payout

    2 | 3 | <%= render partial: 'form_errors', locals: { what: @order_payout } %> 4 | 5 |

    6 | This form is for manually setting vendor or buyer payout information. Normally these fields will be updated automatically 7 | by the payout server connecting to the market api, but this is for situations where you have the payment server turned off 8 | and will use another wallet to manually make payments. 9 |

    10 |

    11 | By manually setting the paid flag and txid field, the user will know 12 | that payment has occurred and it will prevent the payment server from retrieving this record to process a payment. 13 | Make sure the payment server has not already retrieved this record. 14 |

    15 | <%= form_for([:admin, @order_payout], method: 'patch', html: {class: 'form-horizontal'}) do |f| %> 16 |
    17 | <%= f.label :paid, class: 'col-sm-2 control-label' %> 18 |
    19 | <%= f.check_box(:paid, class: 'checkbox') %> 20 |
    21 |
    22 | 23 |
    24 | <%= f.label :txid, class: 'col-sm-2 control-label' %> 25 |
    26 | <%= f.text_field :txid, class: 'form-control' %> 27 |
    28 |
    29 | 30 |
    31 |
    32 | <%= f.submit id: 'submit', class: 'btn btn-primary' %> 33 |
    34 |
    35 | 36 | <%end%> 37 | -------------------------------------------------------------------------------- /app/views/admin/users/index.html.erb: -------------------------------------------------------------------------------- 1 |

    All users (<%=@users.size%>)

    2 | 3 |

    Default sort order is last seen. Param name sort allows sorting by created, updated, username, displayname.

    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | <% @users.each do |user| %> 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | <% end %> 38 | 39 |
    DisplaynameUsernameTypePGP 2FACreatedLast loginUpdatedLoginsFailed logins
    <%= user.displayname %><%= link_to user.username, admin_user_path(user) %><%= user.vendor ? "vendor" : "user" %> 27 | <% if user.pgp_2fa %> 28 | <%=image_tag('checkmark.svg')%> 29 | <%end%> 30 | <%= user.created_at.in_time_zone(admin_user.timezone).to_s(:FHMnozone) %><%= user.lastlogin.in_time_zone(admin_user.timezone).to_s(:FHMnozone) %><%= user.updated_at.in_time_zone(admin_user.timezone).to_s(:FHMnozone) %><%= user.logincount %><%= user.failedlogincount %>
    40 | -------------------------------------------------------------------------------- /lib/gpgkeyinfo.rb: -------------------------------------------------------------------------------- 1 | # ascii message format described here - https://tools.ietf.org/html/rfc4880#page-54 2 | class Gpgkeyinfo 3 | # Fingerprint is displayed on /publickey page. 4 | attr_reader :fingerprint 5 | attr_reader :pubkey 6 | 7 | def initialize(keyid) 8 | @fingerprint = '' 9 | @pubkey = '' 10 | return if keyid.nil? 11 | io = IO.popen("gpg --fingerprint " + keyid) 12 | matches = io.read.match /([0-9A-Z]{4}\s*){10}/ 13 | if matches 14 | @fingerprint = matches[0] 15 | end 16 | io.close 17 | io = IO.popen("gpg -a --export " + keyid) 18 | @pubkey = io.read 19 | io.close 20 | end 21 | 22 | # When a public key is provided on stdin, it outputs a key id data but does not import. 23 | # Newer gpg versions , this is equivalent to gpg --dry-run --import --import-options import-show , but those options have different effect on output when invalid key provided. 24 | # When invalid input, returns stderr "gpg: no valid OpenPGP data found" and no stdout. 25 | # User model has a validation that calls this function and adds an error when this returns empty string. 26 | # Otherwise this is used to present some details about the key on pages like profile, messaging. 27 | # Write stderr to /dev/null to make less test and log output. 28 | def self.read_key(str) 29 | IO.popen("gpg", "r+", :err=>"/dev/null") do |pipe| 30 | pipe.write(str) 31 | pipe.close_write 32 | pipe.read 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/admin/locations_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::LocationsController < ApplicationController 2 | before_action :require_admin 3 | before_action :set_location, only: [:show, :edit, :update, :destroy] 4 | 5 | def index 6 | @locations = Location.all 7 | end 8 | 9 | def new 10 | @location = Location.new 11 | end 12 | 13 | def create 14 | @location = Location.new(location_params) 15 | if @location.save 16 | redirect_to admin_locations_path, notice: 'Location was created' 17 | else 18 | render :new 19 | end 20 | end 21 | 22 | def update 23 | if @location.update(location_params) 24 | redirect_to admin_locations_path, notice: 'Location was updated' 25 | else 26 | render :edit 27 | end 28 | end 29 | 30 | def destroy 31 | # A product has a from_location. We can't delete a location refered to by a product from_location because it breaks ref. integrity. 32 | # A product also has many to-locations (and to-location has many products) defined by HABTM table. 33 | if @location.allow_destroy? && @location.destroy 34 | redirect_to admin_locations_path, notice: 'Location deleted' 35 | else 36 | redirect_to admin_locations_path, alert: 'Location delete failed' 37 | end 38 | end 39 | 40 | private 41 | def set_location 42 | @location = Location.find(params[:id]) 43 | end 44 | 45 | def location_params 46 | params.require(:location).permit(:description) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/views/products/_unitprices.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <% product.unitprices.each do |unitprice| %> 3 | 4 | 5 | 13 | <%if show_price_per_unit%> 14 | <%# price per unit calculation. 15 | The precision setting will automatically round. So 100g/$1.60 has ppu 0.02 $/g. 16 | If ppu below 1c then don't bother displaying because it will be zero. %> 17 | 34 | <%end%> 35 | 36 | <%end%> 37 |
    <%= sprintf("%g", unitprice.unit) %> <%= product.unitdesc %> 6 | <% if current_user %> 7 | <%= to_users_currency(unitprice.currency, unitprice.price) %> <%= current_user.currency %> 8 | <%else%> 9 | <%# just display in whatever currency it was saved as %> 10 | <%= currency_format(unitprice.price) %> <%=unitprice.currency%> 11 | <% end %> 12 | 18 | <% if current_user %> 19 | <% ppu = to_users_currency(unitprice.currency, unitprice.price / unitprice.unit, precision=2) %> 20 | <% unless ppu == "0.00" %> 21 | <%= ppu %> 22 | <%=current_user.currency%> 23 | / <%=unitprice.product.unitdesc %> 24 | <%end%> 25 | <%else%> 26 | <% ppu = number_to_currency(unitprice.price / unitprice.unit, precision: 2, unit: "") %> 27 | <% unless ppu == "0.00" %> 28 | <%= ppu %> 29 | <%=unitprice.currency%> 30 | / <%=unitprice.product.unitdesc %> 31 | <%end%> 32 | <%end%> 33 |
    38 | -------------------------------------------------------------------------------- /app/views/users/pgp_2fa.html.erb: -------------------------------------------------------------------------------- 1 |

    PGP Two factor auth

    2 | 3 | <% if @gpg_msg != nil %> 4 | 5 |

    To enable or disable PGP 2FA you will need to decrypt the message below and enter the decrypted word in the form.

    6 | 7 |

    8 | To copy, click text box and press 9 | ctrl a 10 | ctrl c 11 |

    12 |

    13 | 16 |

    17 | 18 | <%= form_for(@user, method: 'post', url: account_pgp_2fa_path, html: {class: 'form-horizontal'}) do |f| %> 19 |
    20 | Enable PGP 2FA 21 |
    22 |
    23 | 26 |
    27 |

    28 | This will require you to decrypt a random word every time you login and also when changing account settings. 29 |

    30 |
    31 |
    32 | 33 |
    34 | <%= label_tag :secret_word, "Decrypted word", class: 'col-sm-2 control-label' %> 35 |
    36 | <%= text_field_tag :secret_word, nil, class: 'form-control' %> 37 |
    38 |
    39 | 40 |
    41 |
    42 | <%= f.submit "Submit", id: 'submit', class: 'btn btn-primary' %> 43 |
    44 |
    45 | 46 | 47 | <%end%> 48 | 49 | 50 | 51 | <%end%> 52 | -------------------------------------------------------------------------------- /app/controllers/admin/payouts_controller.rb: -------------------------------------------------------------------------------- 1 | # This controller is used on the payout server. 2 | class Admin::PayoutsController < ApplicationController 3 | before_action :require_admin 4 | 5 | def index 6 | # Job will block so index can take a while to load if there are a lot of payouts for the job to check. 7 | begin 8 | PayoutConfirmationsJob.perform_now 9 | rescue 10 | flash.now[:alert] = 'Problem running PayoutConfirmationsJob' 11 | end 12 | @payouts = Payout.order('paid ASC, market_updated ASC, updated_at DESC, username') 13 | @payouts = @payouts.page(params[:page]) 14 | 15 | # For any unpaid payouts, find those users past paid payouts and all the addresses they ever paid out to. 16 | # If an unpaid payout is requesting payment to an address the user has never used before, then we highlight this in the view. 17 | # First get an array of usernames of unpaid payouts. 18 | usernames_unpaid = Payout.where(paid: false).select(:username).distinct.pluck(:username) 19 | # Generate a hash with keys being usernames and value is array of past paid addresses. 20 | @past_paid_addresses = Payout.select('username, json_agg(payout_btc_address)').where(paid: true). 21 | where(username: usernames_unpaid). 22 | group('username'). 23 | pluck(:username, 'json_agg(payout_btc_address)').to_h 24 | # ie: {"vendor1"=>["mn3oydmDskW16ZWLV8Qe7Av7NsH2Mzp6UH", "mxYcACPJWAMMkXu7S9SM8npicFWehpYCWx", "mfsMmGSPkYyrki79yiX6SQGSV2z3eVyq4C"]} 25 | end 26 | 27 | def show 28 | @payout = Payout.find(params[:id]) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/views/feedbacks/_form.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <%= form_for(@feedback) do |f| %> 4 | <%= render partial: 'form_errors', locals: { what: @feedback } %> 5 | 6 | <% if @feedback.new_record? %> 7 | <%# When submitting new feedback (as opposed to an update), the order_id parameter is required. Updates don't need to specify order_id. %> 8 | <%= f.hidden_field(:order_id) %> 9 |
    10 |
    11 | 14 |
    15 |
    16 | 19 |
    20 |
    21 | 24 |
    25 |
    26 | <%end%> 27 | 28 | <%# Updates can only modify the text message, not the rating. Can't remember the exact reasons for designing it this way. 29 | Maybe reason is to prevent someone changing historical positive feedback to negative based on a single bad sale. %> 30 |
    31 | <%= f.label :feedback %> 32 | <%= f.text_area :feedback, rows: 5, class: 'form-control' %> 33 |
    34 |
    35 | <%= f.submit class: 'btn btn-primary' %> 36 |
    37 | <% end %> 38 |
    39 |
    40 | -------------------------------------------------------------------------------- /app/views/admin/categories/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Categories

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @categories.each do |cat| %> 15 | 16 | 17 | <% cat_product_count = cat.products.count %> 18 | 19 | 20 | 33 | 34 | <%end%> 35 | 36 | 37 |
    NameProduct countSort index
    <%= cat.name %><%= cat_product_count %><%= cat.sortorder %> 21 |
    22 | <%# Put the edit link inside form just for formatting so they are one same line %> 23 | <%= form_tag([:admin, cat], method: :delete) do %> 24 | <%= link_to 'Edit', edit_admin_category_path(cat), class: "btn btn-primary btn-sm" %> 25 | <%= submit_tag("Delete", class: "btn btn-danger btn-sm") if cat_product_count == 0 %> 26 | <%# Tried using 'disabled' class to prevent delete when products exist in category but 27 | it would hover a circle crossed out and clicking would still submit the delete action. 28 | disabled css class can be used on links that are displayed as buttons to prevent clicking. 29 | but disabled css class shouldn't be used on inputs like submit button because it appears disabled but still allows clicking.%> 30 | <% end %> 31 |
    32 |
    38 |

    <%= link_to 'Add category', new_admin_category_path, class: "btn btn-primary" %>

    39 | -------------------------------------------------------------------------------- /app/views/admin/messages/conversations.html.erb: -------------------------------------------------------------------------------- 1 |

    Messages: <%=@user.displayname%>

    2 | 3 |

    4 | The message list in this table is exactly what the specified user will see in their views. So if they have deleted messages (references) 5 | then admin will not be able to see the deleted messages either. 6 |

    7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | <% @conversations.each do |conversation| %> 22 | 23 | 24 | 27 | 28 | 29 | 32 | 33 | <% end %> 34 | 35 |
    <%= @user.vendor ? "Buyer name" : "Vendor name" %>New messagesMessage countLast sent/received
    <%= conversation.otherparty.displayname %> 25 | <%= conversation.unseen_cnt %> 26 | <%= conversation.cnt %><%= conversation.newest_message_date.in_time_zone(admin_user.timezone).strftime('%F') %> 30 | <%= link_to 'View', admin_show_conversation_path(@user, conversation.otherparty), class: 'btn btn-primary' %> 31 |
    36 | 37 |

    Summary of all prior converstations

    38 |

    39 | To view messages where all references have been deleted, this list is every user ever sent to or received from. 40 | The list of usernames in the above table is a subset of users in this list. 41 |

    42 | 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml example file for use on market server or payment server. 2 | # The commented sections are only applicable when configuring the market server. 3 | db: 4 | image: postgres 5 | environment: 6 | POSTGRES_PASSWORD: mysecretpassword 7 | trademed: 8 | image: trademed 9 | volumes: 10 | - /home/rails/docker/log:/usr/src/app/log 11 | - /home/rails/docker/public/system:/usr/src/app/public/system 12 | ports: 13 | - "3000:3000" 14 | links: 15 | - db:db.local 16 | environment: &app_env 17 | RAILS_ENV: production 18 | RAILS_SERVE_STATIC_FILES: 'true' 19 | SECRET_KEY_BASE: YOUR_SECRET_HERE 20 | DATABASE_URL: postgres://trademed_prod:PASSWORD@db.local/trademed_prod 21 | LOGO_FILENAME: logo.png 22 | SITENAME: Trademed 23 | GPG_KEY_ID: YOUR_KEY_ID 24 | BLOCKCHAIN_CONFIRMATIONS: 3 25 | ADMIN_API_KEY: YOUR_MARKET_SERVER_KEY 26 | 27 | # Payout only settings. 28 | TOR_PROXY_HOST: 192.168.1.2 29 | TOR_PROXY_PORT: 9050 30 | ADMIN_API_URI_BASE: http://admin.xxxxxxx.onion/ 31 | PAYOUT_BITCOIND_URI: http://bitcoinrpc:YOUR_RPC_PASSWORD_HERE@192.168.1.2:8332/ 32 | PAYOUT_LITECOIND_URI: ... 33 | BITCOIND_ORDERS_ACCOUNT_NAME: trademed_orders 34 | 35 | # Market only settings. 36 | #DISPLAYNAME_HASH_SALT: xxxxxxx 37 | #CURRENCIES: USD AUD EUR GBP CAD 38 | #MARKET_BITCOIND_URI: http://bitcoinrpc:YOUR_RPC_PASSWORD_HERE@192.168.1.2:8332/ 39 | #ENABLE_VENDOR_REGISTRATION_FORM: 'true' 40 | #ENABLE_MANDATORY_PGP_USER_ACCOUNTS: 'true' 41 | #BITCOIND_WATCH_ADDRESS_LABEL: trademed_watches 42 | #ADMIN_HOSTNAME: admin.xxxxxxx.onion 43 | -------------------------------------------------------------------------------- /app/jobs/ltc_rates_coinmarketcap_job.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'json' 3 | 4 | # This requires the bitcoin rates to be set and the litecoin/bitcoin rate 5 | # is used to calculate all the country litecoin rates. Not ideal but this is 6 | # how bitcoin exchange rates are usually calculated by providers rather than 7 | # looking at country specific exchanges. 8 | 9 | class LtcRatesCoinmarketcapJob < ApplicationJob 10 | queue_as :default 11 | 12 | def perform() 13 | if PaymentMethod.litecoin_exists? 14 | ScriptLog.info("#{self.class} - updating litecoin exchange rates") 15 | 16 | uri = URI('https://api.coinmarketcap.com/v1/ticker/litecoin/') 17 | response_string = Net::HTTP.get(uri) 18 | json = JSON.parse(response_string) 19 | 20 | # https://api.coinmarketcap.com/v1/ticker/litecoin/ 21 | #=> [ 22 | # { 23 | # "id": "litecoin", 24 | # "name": "Litecoin", 25 | # "symbol": "LTC", 26 | # "rank": "6", 27 | # "price_usd": "285.406", 28 | # "price_btc": "0.0172719", 29 | 30 | ltc_btc_rate = BigDecimal.new(json[0]['price_btc']) 31 | 32 | ScriptLog.info("#{self.class} - ltc/btc rate is #{ltc_btc_rate}") 33 | 34 | litecoin_key = PaymentMethod.litecoin.id 35 | Rails.configuration.currencies.each do |currency| 36 | btc_rate = PaymentMethod.bitcoin.btc_rates.find_by(code: currency) 37 | raise if btc_rate.nil? 38 | ltc_rate = BtcRate.find_or_create_by!(code: currency, payment_method_id: litecoin_key) 39 | ltc_rate.rate = btc_rate.rate * ltc_btc_rate 40 | ltc_rate.save! 41 | end 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/jobs/update_orders_from_blockchain_job.rb: -------------------------------------------------------------------------------- 1 | # This job updates the database with the amount paid to an order's bitcoin or litecoin address. 2 | # It is important that concurrent execution of this job does not occur. If multiple copies run at same time 3 | # then the variables that hold results from the bitcoin RPC can be currupted, resulting in locked orders. 4 | 5 | # Note the reason we also look at expired and paid orders: 6 | # Expired orders may have one or more payments confirm after expiry time. 7 | # Paid orders may become unpaid due to a blockchain re-org. Ie if you are only doing 1 confirmation (config.blockchain_confirmations), 8 | # an order that has already been marked Paid may have the paid amount change. 9 | # Sometimes a low bitcoin fee can cause an expired order to take over a week to confirm. So retrieve orders created at least 1 week ago. 10 | 11 | 12 | class UpdateOrdersFromBlockchainJob < ApplicationJob 13 | queue_as :default 14 | 15 | def perform() 16 | orders = Order.where('created_at > ?', Time.now - 3.week). 17 | where(status: [Order::PAYMENT_PENDING, Order::EXPIRED, Order::PAID]). 18 | order('created_at') # first in first served. 19 | 20 | # Exclude any with refunds already processed. Not interested in handling this case. 21 | orders = orders.select do |o| 22 | o.buyer_payout == nil || o.buyer_payout.paid == false 23 | end 24 | 25 | ScriptLog.info("#{self.class} - found #{orders.size} orders to check") 26 | 27 | orders.map(&:update_from_blockchain) 28 | 29 | # Looks for an overpaid order that can cover costs of later pending orders, and makes those pending orders paid. 30 | MultipayCheckJob.perform_now 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /app/models/payment_method.rb: -------------------------------------------------------------------------------- 1 | class PaymentMethod < ApplicationRecord 2 | # If a PaymentMethod with name Litecoin exists then additional code does things like 3 | # obtaining litecoin exchange rates and displaying in nav bar. 4 | # The enabled attribute determines whether new orders can choose the payment method. This is 5 | # designed for maintenance situations where you want to temporarily disable new orders for that payment method. 6 | # Products can be edited to allow them to have disabled payment methods, but new orders won't allow that method. 7 | # It does not prevent vendors from selecting disabled PaymentMethods on their products. And product views 8 | # will show disabled payment methods as options. 9 | # Once a payment method exists, it shouldn't be removed because BtcAddresses, Orders, require a payment method attribute 10 | # so deleting a payment method could break integrity. 11 | has_and_belongs_to_many :products 12 | has_many :orders 13 | has_many :btc_rates 14 | has_many :btc_addresses 15 | validates :name, length: { minimum: 1, maximum: 255 } 16 | validates :name, format: { with: /\A[[:print:]]*\z/, message: "unexpected characters" } 17 | validates :name, uniqueness: true 18 | 19 | default_scope { order(:name) } 20 | 21 | # class method to return the Bitcoin payment method. 22 | def self.bitcoin 23 | self.where(name: 'Bitcoin').first 24 | end 25 | 26 | def self.litecoin 27 | self.where(name: 'Litecoin').first 28 | end 29 | 30 | def self.litecoin_exists? 31 | self.litecoin != nil 32 | end 33 | 34 | def self.bitcoin_exists? 35 | self.bitcoin != nil 36 | end 37 | 38 | def is_bitcoin? 39 | name == 'Bitcoin' 40 | end 41 | 42 | def is_litecoin? 43 | name == 'Litecoin' 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/models/shippingoption.rb: -------------------------------------------------------------------------------- 1 | class Shippingoption < ApplicationRecord 2 | # A shipping option can be used on multiple products and can exist when zero products exist. 3 | # Therefore we need to store who created the shipping option. 4 | # Shipping options can exist without a user reference, see orders controller where user_id is set to nil. 5 | has_and_belongs_to_many :products 6 | belongs_to :user, optional: true 7 | has_many :orders 8 | 9 | # Shipping options are permitted to have null user_id - see orders controller#create. 10 | validates :description, length: { minimum: 1, maximum: 100 } 11 | validates :description, format: { with: /\A[[[:print:]]]+\z/, message: "unexpected characters" } 12 | validates :price, numericality: { greater_than_or_equal_to: 0 } 13 | validates :currency, :inclusion => { in: Rails.configuration.currencies } 14 | 15 | before_destroy :check_product_dependencies 16 | 17 | private 18 | # It is not valid for a product to have no shipping options but when you delete all shipping options, 19 | # the product will have no shippingoptions because only HABTM table is changing (not the product) so product validator not triggered. 20 | # This method will cause the destroy method to abort if this shippingoption is an "only child" of some product. 21 | # If the product has been deleted (the deleted attribute true) then we allow the delete of all its shippingoptions. 22 | def check_product_dependencies 23 | # Iterate though the products associated to this SO, that are not deleted. 24 | products.visible.each do |p| 25 | # throw(:abort) will make the destroy method return false and the controller will know the destroy didn't work. 26 | throw(:abort) if p.shippingoptions.count == 1 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/views/admin/products/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Products

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <% @products.each do |product| %> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 38 | 39 | 44 | 45 | <% end %> 46 | 47 | 48 | 49 | 50 |
    Created atVendorTitleCategoryStock QuantityDescEnabledHiddenOrders countPayment methods
    <%= product.created_at.in_time_zone(admin_user.timezone).to_s(:FHMnozone) %><%= link_to truncate(product.vendor.displayname, length:15), admin_profile_path(product.vendor) %><%= link_to product.title, product_path(product) %><%= truncate(product.category.name, length: 10) %><%= product.stock %><%= truncate(product.description, length: 15) %> 29 | <% if product.available_for_sale %> 30 | <%=image_tag('checkmark.svg')%> 31 | <%end%> 32 | 34 | <% if product.hidden %> 35 | <%=image_tag('checkmark.svg')%> 36 | <%end%> 37 | <%= product.orders_count %> 40 | <% product.payment_methods.each do |pm| %> 41 | <%=image_tag("#{pm.name.downcase}.svg", class: "small_logo", width: "16", height: "16")%> 42 | <% end %> 43 |
    Product count: <%=@products_count%>
    51 | <%= paginate @products %> 52 | -------------------------------------------------------------------------------- /examples/environment_example_market.sh: -------------------------------------------------------------------------------- 1 | # When running under docker, these will be set in the docker-compose.yml file 2 | # but if not using docker, this is how the application settings would be specified. 3 | # Example values only. 4 | export RAILS_ENV=production 5 | export RAILS_SERVE_STATIC_FILES=true 6 | export GPG_KEY_ID=ABCDEF12 7 | # set either MARKET_BITCOIND_URI or PAYOUT_BITCOIND_URI, no both. This allows admin area to show different views. 8 | export MARKET_BITCOIND_URI=http://bitcoinrpc:aaaacccc@10.8.0.1:19882/ 9 | export MARKET_LITECOIND_URI=http://litecoinrpc:1111ccceee@10.8.0.1:19338/ 10 | export PAYOUT_BITCOIND_URI= 11 | export PAYOUT_LITECOIND_URI= 12 | export ADMIN_API_KEY=example6030a5093fd715471d4e154469c48dada4cd604bd3e8eb9036aaf561ae44ecf40a79280b4ab9a31b9cd0784b3bd5a8873a99b80cadaeccb1e7d9a85a2 13 | export DATABASE_URL=postgres://trademed_prod:passwordxxxx@localhost/trademed_production 14 | export SITENAME="Gonzos Widgets" 15 | export ADMIN_HOSTNAME=admin.abcede1122334344.onion 16 | export CURRENCIES="USD AUD EUR CNY GBP RUB CAD" 17 | export DEFAULT_CURRENCY="USD" 18 | export DEFAULT_TIMEZONE="London" 19 | export DISPLAYNAME_HASH_SALT="example6030a5093fd715471d4e154469c48" 20 | export LOGO_FILENAME=logo.png 21 | export ENABLE_SUPPORT_TICKETS=TRUE 22 | # number of confirmations before marked paid and stock reduced. 23 | export BLOCKCHAIN_CONFIRMATIONS=3 24 | export COMMISSION=0.05 25 | export ORDERS_PER_HOUR_THRESHOLD=5 26 | export LISTTRANSACTIONS_COUNT=12000 27 | export EXPIRE_UNPAID_ORDER_DURATION=86400 28 | export SECRET_KEY_BASE=example3f50a06afa3637dec84548229dda371683860750867815092c9487a36d4452a507580e0557c490860859c6d6b7e194089caa7ef9427db4d4dd4733014 29 | export ORDER_AGE_TO_HIDE_PRICE_ON_REVIEW=27648000 30 | export ENABLE_VENDOR_REGISTRATION_FORM=TRUE 31 | export ENABLE_MANDATORY_PGP_USER_ACCOUNTS=TRUE 32 | -------------------------------------------------------------------------------- /app/models/order_payout.rb: -------------------------------------------------------------------------------- 1 | # An OrderPayout is only created when order status changes to a final state. See order model. 2 | # In a finalized order, a single OP is created for the vendor. 3 | # All expired orders have a single OP regardless of whether there is anything to refund. It was simpler to code this way. 4 | # For partial refunds, the order will have two associated OrderPayouts - a buyer and a vendor OrderPayout. 5 | # Users can alter the payout address any time before it has been paid, even if set automatically with the address defined in account settings. 6 | # For security, considered making it immutable once set but not really worth it because the window of time from being finalized to being paid 7 | # is small so attacker (phisher) won't have many order payouts available to change. 8 | class OrderPayout < ApplicationRecord 9 | validate :address_validate, unless: Proc.new { btc_address.nil? } 10 | # Allows lookup of username directly from the order_payout. This is simply to make the query easier to write in order_payouts_api_controller.rb 11 | belongs_to :user 12 | belongs_to :order 13 | validates :payout_type, inclusion: { in: %w,vendor buyer, } # so you don't need to find this info from looking at order-user relationship. 14 | validates :txid, format: { with: /\A[a-f0-9]{64}\z/, message: "unexpected characters" }, unless: Proc.new { txid.nil? } # 256bit hash. 15 | 16 | def address_validate 17 | if order.payment_method.is_bitcoin? 18 | valid = Market_bitcoinrpc.validateaddress(btc_address)["isvalid"] 19 | elsif order.payment_method.is_litecoin? 20 | valid = Market_litecoinrpc.validateaddress(btc_address)["isvalid"] 21 | else 22 | valid = false 23 | end 24 | unless valid 25 | errors.add(:btc_address, 'payout address appears invalid') 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 4 | gem 'rails', '5.2.2' 5 | # Use postgresql as the database for Active Record 6 | gem 'pg' 7 | gem 'bootstrap-sass', '~> 3.3.3' 8 | # Use SCSS for stylesheets 9 | gem 'sass-rails', '~> 5.0' 10 | # Use Uglifier as compressor for JavaScript assets 11 | gem 'uglifier', '>= 1.3.0' 12 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 13 | # gem 'therubyracer', platforms: :ruby 14 | 15 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 16 | gem 'turbolinks' 17 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 18 | gem 'jbuilder', '~> 2.0' 19 | # bundle exec rake doc:rails generates the API under doc/api. 20 | gem 'sdoc', '~> 0.4.0', group: :doc 21 | 22 | # Use ActiveModel has_secure_password 23 | # gem 'bcrypt', '~> 3.1.7' 24 | 25 | # Use Unicorn as the app server 26 | # gem 'unicorn' 27 | 28 | # Use Capistrano for deployment 29 | # gem 'capistrano-rails', group: :development 30 | 31 | gem 'thin' 32 | gem 'execjs' 33 | gem 'therubyracer' 34 | gem 'bcrypt' 35 | gem 'friendly_id' 36 | gem 'humanizer' 37 | # Deprecated gem. release notes - https://github.com/thoughtbot/paperclip/blob/master/NEWS 38 | gem "paperclip", "~> 5.3" 39 | 40 | gem 'socksify' 41 | gem 'kaminari' 42 | 43 | group :development, :test do 44 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 45 | gem 'byebug' 46 | 47 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 48 | gem 'spring' 49 | 50 | gem 'rspec-rails' 51 | gem "factory_bot_rails" 52 | gem "capybara" 53 | gem "guard-rspec" 54 | gem 'shoulda-matchers' 55 | gem 'rails-controller-testing' 56 | end 57 | 58 | -------------------------------------------------------------------------------- /app/views/vendor/products/show.html.erb: -------------------------------------------------------------------------------- 1 | <% if @product.hidden %> 2 |
    3 |

    This product is currently hidden from listings. Hidden url for customers is:

    4 |

    <%=product_url(@product)%>

    5 |
    6 | <%end%> 7 | <%= render partial: 'products/product', locals: { product: @product } %> 8 | 9 | <%= link_to 'Edit', edit_vendor_product_path(@product), class: 'btn btn-primary form-button' %> 10 | <%= link_to 'Clone', '#clone', class: 'btn btn-primary form-button' %> 11 | <%= link_to 'Delete', '#confirm_delete', class: 'btn btn-danger form-button' %> 12 | 13 | 27 | 28 | 48 | -------------------------------------------------------------------------------- /app/controllers/shippingoptions_controller.rb: -------------------------------------------------------------------------------- 1 | class ShippingoptionsController < ApplicationController 2 | before_action :set_shippingoption, only: [:show, :edit, :update, :destroy] 3 | before_action :require_vendor 4 | 5 | def index 6 | @shippingoptions = current_user.shippingoptions 7 | end 8 | 9 | def show 10 | end 11 | 12 | def new 13 | @shippingoption = Shippingoption.new 14 | end 15 | 16 | def edit 17 | end 18 | 19 | def create 20 | @shippingoption = Shippingoption.new(shippingoption_params) 21 | @shippingoption.user = current_user 22 | # Instead of before_create callback to set this, do it here. Not all shippingoptions have a user_id due to dup(). 23 | @shippingoption.currency = current_user.currency 24 | 25 | if @shippingoption.save 26 | redirect_to shippingoptions_path, notice: 'Shipping option was created.' 27 | else 28 | render :new 29 | end 30 | end 31 | 32 | def update 33 | if @shippingoption.update(shippingoption_params) 34 | redirect_to shippingoptions_path, notice: 'Shipping option was updated' 35 | else 36 | render :edit 37 | end 38 | end 39 | 40 | def destroy 41 | if @shippingoption.destroy 42 | redirect_to shippingoptions_url, notice: 'Shipping option was deleted' 43 | else 44 | # A callback aborted. 45 | redirect_to shippingoption_path(@shippingoption), 46 | alert: "Cannot delete shipping option. At least one product may require this shipping option because it is the only shipping option it has." 47 | end 48 | end 49 | 50 | private 51 | def set_shippingoption 52 | @shippingoption = Shippingoption.find(params[:id]) 53 | raise NotAuthorized if @shippingoption.user != current_user 54 | end 55 | 56 | def shippingoption_params 57 | params.require(:shippingoption).permit(:description, :price) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/controllers/admin/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::UsersController < ApplicationController 2 | before_action :require_admin 3 | before_action :set_user, only: [:show, :update] 4 | 5 | def index 6 | @users = User.order('lastseen DESC') 7 | if params[:filter] == 'vendor' 8 | @users = @users.where(vendor: true) 9 | end 10 | if params[:sort] == 'created' 11 | @users = @users.reorder('created_at DESC') 12 | end 13 | if params[:sort] == 'updated' 14 | @users = @users.reorder('updated_at DESC') 15 | end 16 | if params[:sort] == 'username' 17 | @users = @users.reorder(:username) 18 | end 19 | if params[:sort] == 'displayname' 20 | @users = @users.reorder(:displayname) 21 | end 22 | end 23 | 24 | def show 25 | @revenue = @user.revenue(admin_user.currency) 26 | end 27 | 28 | # Using render instead of just linking to standard users controller because that controller 29 | # doesn't allow for admin access. 30 | def profile 31 | @user = User.find params[:id] 32 | if @user.is_vendor? 33 | @last_finalized_order = @user.received_orders.finalized.order(:finalized_at).last 34 | end 35 | render 'users/profile' 36 | end 37 | 38 | def update 39 | if user_params[:disabled_until] == 'permanent' 40 | user_params[:disabled_until] = nil 41 | end 42 | # Commission attribute submitted as string but conversion to BigDecimal done automatically. 43 | if @user.update(user_params) 44 | redirect_to admin_user_path(@user), notice: 'Settings saved' 45 | else 46 | render action: 'show' 47 | end 48 | 49 | end 50 | 51 | private 52 | def set_user 53 | @user = User.find(params[:id]) 54 | end 55 | 56 | def user_params 57 | params.require(:user).permit(:vendor, :password, :password_confirmation, :disabled, :disabled_until, :commission) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/views/vendor/order_payouts/_table.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% order_payouts.each do |op| %> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <%# btc_address, txid are normally set once order_payout paid but could be set to nil again for privacy reasons. %> 25 | 26 | 27 | 28 | 29 | 30 | <%end%> 31 | 32 | <% if order_payouts.size > 0 %> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | <%end%> 43 |
    Order idOrder dateProduct<%=currency%> priceCommissionPayment<%=currency%> addresstxidDate paid
    <%= link_to op.order_id[/\w+/], vendor_order_path(op.order_id) %><%= op.order_created.in_time_zone(current_user.timezone).strftime('%F') %><%= truncate(op.title, length:15) %><%= op.btc_price %><%= op.commission %><%= op.btc_amount %><%= op.btc_address[0..10] if op.btc_address %><%= op.txid[0..10] if op.txid %><%= op.updated_at.in_time_zone(current_user.timezone).strftime('%F') %>
    Totals:<%= summary[0].total_orders %><%= summary[0].total_commissions %><%= summary[0].total_payouts %>
    44 | -------------------------------------------------------------------------------- /app/models/btc_address.rb: -------------------------------------------------------------------------------- 1 | # Addresses for customers to make an order payment. 2 | class BtcAddress < ApplicationRecord 3 | belongs_to :order, optional: true 4 | # This field payment_method_id describes what type of address this is - Bitcoin or Litecoin. 5 | belongs_to :payment_method 6 | 7 | scope :unassigned, -> { where(order_id: nil) } 8 | scope :assigned, -> { where.not(order_id: nil) } 9 | scope :signed, -> { where("pgp_signature LIKE '-----BEGIN PGP SIGNED MESSAGE%'") } 10 | scope :bitcoin, -> { includes('payment_method').where(payment_methods: {name: 'Bitcoin'}) } 11 | scope :litecoin, -> { includes('payment_method').where(payment_methods: {name: 'Litecoin'}) } 12 | 13 | validates :address, uniqueness: true 14 | validate :address_valid? 15 | 16 | # If this callback raises, the transaction is rolled back and object not created. 17 | # The rollback is important because we don't want an address in the DB that bitcoind is unaware of. 18 | after_create :add_bitcoind_watch_address 19 | 20 | def add_bitcoind_watch_address 21 | # Don't need to rescan blockchain because the address is brand new it so won't have ever received btc. 22 | if payment_method.is_bitcoin? 23 | Market_bitcoinrpc.importaddress(address, Rails.configuration.bitcoind_watch_address_label, false) 24 | elsif payment_method.is_litecoin? 25 | Market_litecoinrpc.importaddress(address, Rails.configuration.bitcoind_watch_address_label, false) 26 | else 27 | raise 28 | end 29 | end 30 | 31 | protected 32 | # returns whether the string is a bitcoin address. 33 | def address_valid? 34 | return false if payment_method.nil? 35 | if payment_method.is_bitcoin? 36 | Market_bitcoinrpc.validateaddress(address)["isvalid"] 37 | elsif payment_method.is_litecoin? 38 | Market_litecoinrpc.validateaddress(address)["isvalid"] 39 | else 40 | false 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/gpg_operations.rb: -------------------------------------------------------------------------------- 1 | # After import we are expecting to read back something like this 2 | # gpg: key 84CC7D38: "example666 " not changed 3 | # gpg: Total number processed: 1 4 | # gpg: unchanged: 1 5 | 6 | # ascii message format described here - https://tools.ietf.org/html/rfc4880#page-54 7 | 8 | # Normally user keys are not save to gpg keyring but when they use 2FA then their key is saved. 9 | class GpgOperations 10 | attr_reader :key_id 11 | 12 | def initialize(public_key) 13 | @key_id = '' 14 | # Import pub key into the keyring of user running rails server process. 15 | # The only way to encrypt to some key is to have the key in the keyring. 16 | # Output result of import goes to stderr by default so --logger-fd makes it go to stdout. 17 | result = IO.popen("gpg --import --logger-fd 1", "r+") do |pipe| 18 | pipe.write(public_key) 19 | pipe.close_write 20 | pipe.read 21 | end 22 | matches = result.match /gpg: key ([\d\w]+):/ 23 | if matches && matches[1] 24 | @key_id = matches[1] 25 | end 26 | end 27 | 28 | # Output ascii enamored PGP message. 29 | def encrypt(str) 30 | return nil if @key_id.empty? 31 | # The option --trust-model always omits the requirement for you to type y about trusting this key. 32 | result = IO.popen("gpg --trust-model always -e -a -r #{@key_id}", "r+") do |pipe| 33 | pipe.write(str) 34 | pipe.close_write 35 | pipe.read 36 | end 37 | result[/-----BEGIN PGP MESSAGE-----/] ? result : nil 38 | end 39 | 40 | # Used by rspec tests. 41 | # Requires the private key that message encrypted with exist in key ring already. 42 | def self.decrypt(str) 43 | # The option -q omits showing what key it was encrypted with so test output cleaner. 44 | IO.popen("gpg -q -d", "r+") do |pipe| 45 | pipe.write(str) 46 | pipe.close_write 47 | pipe.read 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/views/tickets/index.html.erb: -------------------------------------------------------------------------------- 1 |

    Support

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @tickets.each do |ticket| %> 15 | 16 | 17 | 18 | 25 | 31 | 32 | <% end %> 33 | 34 |
    DateTitleStatus
    <%= ticket.created_at.in_time_zone(current_user.timezone).strftime('%F') %><%= ticket.title %> 19 | <% labeltype = (ticket.status == 'closed') ? 'warning' : 'primary' %> 20 | <%= ticket.status %> 21 | <% if ticket.ticket_messages.where(response_seen: false).count > 0 %> 22 | unseen 23 | <%end%> 24 | 26 |
    27 | <%= link_to 'Details', ticket, class: 'btn btn-primary btn-sm' %> 28 | <%= link_to 'Delete', "#delete_#{ticket.id}", class: 'btn btn-danger btn-sm' %> 29 |
    30 |
    35 | 36 |

    37 | <%= link_to 'Create ticket', new_ticket_path, class: 'btn btn-primary form-button' %> 38 |

    39 |

    40 | For questions about products, send a <%=link_to 'message', conversations_path %> to the product vendor. 41 |

    42 |

    43 | For encrypting support messages please use <%=link_to 'this key', publickey_path %>. 44 |

    45 | 46 | 47 | <% @tickets.each do |ticket| %> 48 | 62 | <% end %> 63 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This is for building production images. Development environment gems are not installed. 2 | 3 | # Before building image, copy logo file to app/assets/images/ and gpgkeyimport.txt to main directory. 4 | 5 | # It is not efficient to use rails:onbuild image because every change to the app's source code will mean 6 | # every step in this Dockerfile needs to run again. This is because the ONBUILD commands are run 7 | # before anything in the Dockerfile. 8 | FROM ruby:2.5 9 | 10 | # throw errors if Gemfile has been modified since Gemfile.lock 11 | RUN bundle config --global frozen 1 12 | 13 | RUN groupadd rails && useradd --create-home -g rails rails 14 | RUN mkdir -p /usr/src/app 15 | WORKDIR /usr/src/app 16 | 17 | RUN apt-get update && apt-get install -y gnupg --no-install-recommends && rm -rf /var/lib/apt/lists/* 18 | #RUN apt-get update && apt-get install -y nodejs --no-install-recommends && rm -rf /var/lib/apt/lists/* 19 | RUN apt-get update && apt-get install -y mysql-client postgresql-client sqlite3 --no-install-recommends && rm -rf /var/lib/apt/lists/* 20 | 21 | COPY Gemfile /usr/src/app/ 22 | COPY Gemfile.lock /usr/src/app/ 23 | RUN bundle install --without development test 24 | 25 | COPY . /usr/src/app 26 | # The config option in development.rb means 'listen' gem is required and "rake assets:precompile" won't run when that gem is missing. 27 | # Removing that line prevents the dependence on 'listen' gem when running commands in non-production environment. 28 | # Other workaround is to run rake with RAILS_ENV=production SECRET_KEY_BASE=anything env. 29 | RUN sed -i '/ActiveSupport::EventedFileUpdateChecker/d' config/environments/development.rb 30 | RUN chown -R rails:rails /usr/src/app 31 | 32 | # CVE-2016–3714 33 | COPY imagemagick-policy.xml /etc/ImageMagick-6/policy.xml 34 | 35 | USER rails 36 | RUN mkdir -p tmp/pids 37 | RUN gpg --import gpgkeyimport.txt 38 | RUN echo "personal-digest-preferences SHA512 SHA384 SHA256\nno-emit-version" > ~/.gnupg/gpg.conf 39 | # this is for use in prod instances but doesn't hurt having them in dev. 40 | RUN bundle exec rake assets:precompile 41 | 42 | EXPOSE 3000 43 | CMD ["rails", "server", "-b", "0.0.0.0"] 44 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | # This method is used by views. Application controller has same method defined too. 3 | def admin_controller? 4 | controller.class.name.split("::").first=="Admin" 5 | end 6 | 7 | def currency_format(number, precision=2) 8 | # Round cent values based on precision, format with commas, but don't show currency symbol. 9 | number_to_currency(number, unit: "", precision: precision) 10 | end 11 | 12 | # Vendors save product prices in their preferred currency. 13 | # When others view these prices, a conversion is done to the current user's preference currency. 14 | # Do not use this function for conversions related to an order because each order has its own exchange rates. 15 | def to_users_currency(fromCurrency, price, precision=2) 16 | fromRate = PaymentMethod.bitcoin.btc_rates.find_by(code: fromCurrency).rate 17 | toRate = PaymentMethod.bitcoin.btc_rates.find_by(code: current_user.currency).rate 18 | number_to_currency( (price * toRate ) / fromRate, unit: "", precision: precision) 19 | end 20 | 21 | def number_to_range(num, ranges) 22 | ranges.each do |range| 23 | if range.include?(num) 24 | return "#{range.first}-#{range.last}" 25 | end 26 | end 27 | return "over #{ranges.last.last}" 28 | end 29 | 30 | # For new order form. 31 | def unitprice_label(unitprice) 32 | "#{sprintf("%g", unitprice.unit)} #{unitprice.product.unitdesc} - #{to_users_currency(unitprice.currency, unitprice.price)} #{current_user.currency}" 33 | end 34 | 35 | # The lastseen attribute of a user is a string that logs in UTC the last hour they were active. 36 | # This converts that string into another string representing the time in the current user's timezone. 37 | # Historically lastseen was displayed directly in views as UTC for simplicity but nicer to convert to users timezone. 38 | # lastseen may be nil, so to avoid exception on parse, check this case. 39 | def lastseen_to_current_users_timezone(lastseen) 40 | if lastseen 41 | t = Time.zone.parse(lastseen) 42 | t.in_time_zone(session_user.timezone).to_s(:FHM) 43 | else 44 | "n/a" 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/views/admin/btc_address/search_form.html.erb: -------------------------------------------------------------------------------- 1 |

    Market bitcoin addresses

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    Assigned to orders<%= PaymentMethod.bitcoin.btc_addresses.assigned.count %>
    Available (unassigned to orders)<%= PaymentMethod.bitcoin.btc_addresses.unassigned.count %>
    PGP signature present<%= PaymentMethod.bitcoin.btc_addresses.signed.count %>
    Total count<%= PaymentMethod.bitcoin.btc_addresses.count %>
    17 | 18 | <% if PaymentMethod.litecoin_exists? %> 19 |

    Market litecoin addresses

    20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
    Assigned to orders<%= PaymentMethod.litecoin.btc_addresses.assigned.count %>
    Available (unassigned to orders)<%= PaymentMethod.litecoin.btc_addresses.unassigned.count %>
    PGP signature present<%= PaymentMethod.litecoin.btc_addresses.signed.count %>
    Total count<%= PaymentMethod.litecoin.btc_addresses.count %>
    35 | <% end %> 36 | 37 |

    Search

    38 | <%=form_tag admin_btc_address_search_path, class: 'form-horizontal' do %> 39 |
    40 | <%= label_tag :btc_address, 'Address', class: 'col-sm-2 control-label' %> 41 |
    42 | <%= text_field_tag :btc_address, nil, class: 'form-control' %> 43 |
    44 |
    45 |
    46 |
    47 | <%= submit_tag "Search", class: 'btn btn-default' %> 48 |
    49 |
    50 | <%end%> 51 | 52 | <% if @btc_address %> 53 |

    54 | <%= @btc_address.address %> 55 |

    56 | 57 |
    58 | <%= @btc_address.pgp_signature %>
    59 |   
    60 | 61 |

    62 | <%if @btc_address.order %> 63 | Assigned to <%= link_to 'order', admin_order_path(@btc_address.order) %> 64 | <%else%> 65 | Available to be assigned to a new order. 66 | <%end%> 67 |

    68 | <% end %> 69 | -------------------------------------------------------------------------------- /app/controllers/tickets_controller.rb: -------------------------------------------------------------------------------- 1 | class TicketsController < ApplicationController 2 | before_action :require_login 3 | before_action :set_ticket, only: [:show, :update, :destroy] 4 | 5 | def index 6 | @tickets = current_user.tickets.order('created_at DESC, status DESC') 7 | end 8 | 9 | def new 10 | @ticket = Ticket.new 11 | @ticket.ticket_messages.build(message: '') 12 | end 13 | 14 | # A ticket has one or more ticket messages. New text is added by adding another ticket message (TM). 15 | # Using accepts_nested_attributes_for to make saving the TM easier. 16 | def create 17 | @ticket = Ticket.new(ticket_params) 18 | @ticket.user = current_user 19 | @ticket.status = 'open' 20 | if @ticket.save 21 | redirect_to @ticket, notice: 'Ticket was created' 22 | else 23 | # If user submitted the form without a message, then we need to build one for the view. See model. 24 | @ticket.ticket_messages.build(message: '') if @ticket.ticket_messages.empty? 25 | render :new 26 | end 27 | end 28 | 29 | def show 30 | # Form will allow saving a new message. 31 | @ticket.ticket_messages.build(message: '') 32 | @ticket.ticket_messages.update_all response_seen: true 33 | end 34 | 35 | def update 36 | # Creates a new ticket_message with message attribute set from form field. 37 | if @ticket.update(ticket_params) 38 | redirect_to @ticket, notice: 'Support ticket saved' 39 | else 40 | render :show 41 | end 42 | end 43 | 44 | def destroy 45 | @ticket.destroy 46 | redirect_to tickets_path, notice: 'Support ticket was deleted' 47 | end 48 | 49 | private 50 | def set_ticket 51 | @ticket = Ticket.find(params[:id]) 52 | raise NotAuthorized unless @ticket.user == current_user 53 | end 54 | 55 | # Title is readonly so doesn't matter if user attempts to edit it. 56 | def ticket_params 57 | # We only add new TicketMessages, never edit existing ones so no need to allow id in ticket_message_attributes. 58 | # Since no TicketMessage ids received, no need to check if owned by user. 59 | params.require(:ticket).permit(:title, :status, ticket_messages_attributes: [:message]) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/views/application/_admin_nav.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | Logged in as: <%=admin_user.username%> 4 | (<%= admin_user.displayname %>) 5 |
    6 |
    7 | 44 |
    45 |
    46 | 47 | 60 | -------------------------------------------------------------------------------- /app/views/orders/multipay.html.erb: -------------------------------------------------------------------------------- 1 |

    Multipay

    2 | 3 | <%# @orders has at least 2 payment pending orders. All of them have same payment method. %> 4 |

    5 | This feature allows you to pay multiple orders using a single payment to one <%=@primary_order.payment_method.name%> address. 6 | This is an advanced feature because it requires you to pay the exact amount specified. If you are not confident in wallet operation to ensure the exact amount is paid, 7 | then avoid this feature and pay each order separately. 8 |

    9 | 10 |

    11 | Please pay <%=@total_to_pay%> <%=@primary_order.payment_method.code%> to <%=@primary_order.btc_address.address%> 12 |

    13 |

    14 | This is the address of your first (oldest) payment pending order. 15 | Once you have paid the amount specified, do not make additional payments to the other orders. 16 | When the payment has sufficient confirmations, all your orders shown here will change to paid. 17 | If you pay too much or pay less than specified, this feature will not work and only the first order will receive the payment. 18 |

    19 | 20 |

    21 |   22 |

    23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | <% @orders.each do |order| %> 36 | 37 | 38 | 39 | 40 | <%# Price ([in users currency]) %> 41 | <%# Pay price %> 42 | 43 | <%end%> 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 |
    Create timeProductVendorPrice (<%= current_user.currency %>)Pay Price
    <%= order.created_at.in_time_zone(current_user.timezone).to_s(:FHMnozone) %><%= order.title %><%= link_to truncate(order.vendor.displayname, length:15), profile_path(order.vendor) %><%= currency_format(order.total_price_in_currency(current_user.currency)) %>  <%= order.btc_price %>
    Total to pay: 49 | <%=@total_to_pay%> 50 | <%=image_tag("#{@primary_order.payment_method.name.downcase}.svg", class: "small_logo", width: "16", height: "16")%> 51 |
    55 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 18 | config.action_controller.perform_caching = true 19 | 20 | config.cache_store = :memory_store 21 | config.public_file_server.headers = { 22 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 23 | } 24 | else 25 | config.action_controller.perform_caching = false 26 | 27 | config.cache_store = :null_store 28 | end 29 | 30 | # Store uploaded files on the local file system (see config/storage.yml for options) 31 | config.active_storage.service = :local 32 | 33 | # Don't care if the mailer can't send. 34 | config.action_mailer.raise_delivery_errors = false 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Print deprecation notices to the Rails logger. 39 | config.active_support.deprecation = :log 40 | 41 | # Raise an error on page load if there are pending migrations. 42 | config.active_record.migration_error = :page_load 43 | 44 | # Highlight code that triggered database queries in logs. 45 | config.active_record.verbose_query_logs = true 46 | 47 | # Debug mode disables concatenation and preprocessing of assets. 48 | # This option may cause significant delays in view rendering with a large 49 | # number of complex assets. 50 | config.assets.debug = true 51 | 52 | # Suppress logger output for asset requests. 53 | config.assets.quiet = true 54 | 55 | # Raises error for missing translations 56 | # config.action_view.raise_on_missing_translations = true 57 | 58 | # Use an evented file watcher to asynchronously detect changes in source code, 59 | # routes, locales, etc. This feature depends on the listen gem. 60 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 61 | end 62 | -------------------------------------------------------------------------------- /app/models/generated_address.rb: -------------------------------------------------------------------------------- 1 | # Used on payout server when creating bitcoin address. 2 | class GeneratedAddress < ApplicationRecord 3 | validates :btc_address, uniqueness: true 4 | validates :address_type, inclusion: { in: %w(LTC BTC) } 5 | validate :address_valid? 6 | # Try to save all created bitcoin addresses to DB even if problem using pgp and pgp_signature is empty. 7 | # The web interface of payout server can show if any GeneratedAddresses have nil or empty pgp_signature, 8 | # these can be corrected manually on console to ensure a signature exists or discarded. 9 | validates :pgp_signature, format: { with: /\A[[[:print:]]\r\n]*\z/, message: "unexpected characters" } 10 | 11 | scope :loaded_to_market, -> { where(loaded_to_market: true) } 12 | scope :uploadable, -> { where(loaded_to_market: false) } 13 | scope :signed, -> { where("pgp_signature LIKE '-----BEGIN PGP SIGNED MESSAGE%'") } 14 | scope :bitcoin, -> { where(address_type: 'BTC') } 15 | scope :litecoin, -> { where(address_type: 'LTC') } 16 | 17 | # You need to ensure gpg doesn't have passphrase or else get environment setup with gpg agent info. 18 | # If using agent, note signing uses a different key to decryption so do an initial clearsign on console to make agent cache passphrase. 19 | def sign 20 | gpg_key_id = Rails.configuration.gpg_key_id 21 | # The batch option just prevents the tty output about passphrase required which is annoying. 22 | self.pgp_signature = IO.popen("gpg --clearsign --batch --default-key #{gpg_key_id}", 'r+') do |pipe| 23 | pipe.write(btc_address + "\n") 24 | pipe.close_write() 25 | pipe.read() 26 | end 27 | end 28 | 29 | # Requires address_type attribute to be set on the instance before calling this method. 30 | def generate 31 | if address_type == 'BTC' 32 | rpc = Payout_bitcoinrpc 33 | elsif address_type == 'LTC' 34 | rpc = Payout_litecoinrpc 35 | end 36 | addr = rpc.getnewaddress(Rails.configuration.bitcoind_order_address_label) 37 | self.btc_address = addr 38 | end 39 | 40 | protected 41 | # returns whether the string is a bitcoin address. 42 | def address_valid? 43 | if address_type == 'BTC' 44 | Payout_bitcoinrpc.validateaddress(btc_address)["isvalid"] 45 | elsif address_type == 'LTC' 46 | Payout_litecoinrpc.validateaddress(btc_address)["isvalid"] 47 | else 48 | false 49 | end 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/bitcoin_rpc.rb: -------------------------------------------------------------------------------- 1 | # https://en.bitcoin.it/wiki/API_reference_%28JSON-RPC%29#Ruby 2 | # If you see this "EOFError: end of file reached" in the log it means the port was closed. 3 | # This "Net::ReadTimeout: Net::ReadTimeout" can mean the connect was successful but bitcoind didn't write a response back. ie too busy. 4 | class BitcoinRPC 5 | 6 | def initialize(service_url) 7 | @uri = URI.parse(service_url) 8 | end 9 | 10 | def method_missing(name, *args) 11 | post_body = { 'method' => name, 'params' => args, 'id' => 'jsonrpc' }.to_json 12 | responsebody = http_post_request(post_body) 13 | # JSON::ParserError raised if response not json such as empty string from HTTP 403. 14 | resp = JSON.parse( responsebody ) 15 | raise(JSONRPCError, resp['error']) if resp['error'] 16 | 17 | # Hack to return BigDecimal instead of Float. 18 | if resp["result"].is_a?(Float) 19 | match = responsebody.match %r|"result":([\d\.]+)| 20 | BigDecimal.new(match[1]) 21 | else 22 | resp['result'] 23 | end 24 | end 25 | 26 | def http_post_request(post_body) 27 | http = Net::HTTP.new(@uri.host, @uri.port) 28 | request = Net::HTTP::Post.new(@uri.request_uri) 29 | request.basic_auth @uri.user, @uri.password 30 | request.content_type = 'application/json' 31 | request.body = post_body 32 | http.request(request).body 33 | end 34 | 35 | # Return sum of transaction amounts paid to specified address, 36 | # each having at least confirmations, and were created no later than time. 37 | def getreceivedbyaddress_at_time(address, confirmations, time) 38 | # It includes unconfirmed transactions as well. 39 | # blocktime parameter is not present in unconfirmed transactions but they have a time parameter (broadcast time). 40 | transactions = self.listtransactions(::DummyStar, Rails.configuration.listtransactions_count, 0, true) 41 | 42 | total_received = BigDecimal.new(0) 43 | 44 | transactions.each do |t| 45 | time_attr = t["confirmations"] == 0 ? 'time' : 'blocktime' 46 | if t["address"] == address && 47 | t["confirmations"] >= confirmations && 48 | t[time_attr] <= time.to_i 49 | 50 | total_received += BigDecimal.new(t["amount"].to_s) 51 | end 52 | end 53 | return total_received 54 | end 55 | 56 | def get_version 57 | self.getnetworkinfo['version'] 58 | end 59 | 60 | class JSONRPCError < RuntimeError; end 61 | end 62 | -------------------------------------------------------------------------------- /app/views/admin/generated_address/search_form.html.erb: -------------------------------------------------------------------------------- 1 |

    Generated bitcoin addresses

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    Loaded to market<%= GeneratedAddress.bitcoin.loaded_to_market.count %>
    Not yet loaded to market<%= GeneratedAddress.bitcoin.where(loaded_to_market: false).count %>
    Addresses with PGP signature present<%= GeneratedAddress.bitcoin.signed.count %>
    Addresses with nil PGP signature<%= GeneratedAddress.bitcoin.where(pgp_signature: nil).count %>
    Total count of bitcoin addresses generated by payment server<%= GeneratedAddress.bitcoin.all.count %>
    19 | 20 | <% if PaymentMethod.litecoin_exists? %> 21 |

    Generated litecoin addresses

    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
    Loaded to market<%= GeneratedAddress.litecoin.loaded_to_market.count %>
    Not yet loaded to market<%= GeneratedAddress.litecoin.where(loaded_to_market: false).count %>
    Addresses with PGP signature present<%= GeneratedAddress.litecoin.signed.count %>
    Addresses with nil PGP signature<%= GeneratedAddress.litecoin.where(pgp_signature: nil).count %>
    Total count of litecoin addresses generated by payment server<%= GeneratedAddress.litecoin.all.count %>
    39 | <%end%> 40 | 41 |

    Search

    42 | <%=form_tag admin_generated_address_search_path, class: 'form-horizontal' do %> 43 |
    44 | <%= label_tag :btc_address, nil, class: 'col-sm-2 control-label' %> 45 |
    46 | <%= text_field_tag :btc_address, nil, class: 'form-control' %> 47 |
    48 |
    49 |
    50 |
    51 | <%= submit_tag "Search", class: 'btn btn-primary' %> 52 |
    53 |
    54 | <%end%> 55 | 56 | <% if @btc_address %> 57 |

    58 | <%= @btc_address.btc_address %> 59 |

    60 | 61 |
    62 | <%= @btc_address.pgp_signature %>
    63 |   
    64 | 65 | uploaded: <%=@btc_address.loaded_to_market%> 66 | 67 | <% end %> 68 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory 32 | config.active_storage.service = :test 33 | 34 | config.action_mailer.perform_caching = false 35 | 36 | # Tell Action Mailer not to deliver emails to the real world. 37 | # The :test delivery method accumulates sent emails in the 38 | # ActionMailer::Base.deliveries array. 39 | config.action_mailer.delivery_method = :test 40 | 41 | # Print deprecation notices to the stderr. 42 | config.active_support.deprecation = :stderr 43 | 44 | # Raises error for missing translations 45 | # config.action_view.raise_on_missing_translations = true 46 | 47 | ## Instead of saving images files to public/system where they will remain forever, save them here. 48 | ## Since the hash has timestamp as an input the number of images in the directory will increase on every test. 49 | ## spec_helper.rb will delete test_files directory automatically. 50 | Paperclip::Attachment.default_options[:path] = "#{Rails.root}/spec/test_files/:hash.:extension" 51 | 52 | end 53 | -------------------------------------------------------------------------------- /check_bitcoind_watches_setup.rb: -------------------------------------------------------------------------------- 1 | # This script runs on the market server and verifies that all the bitcoin addresses in the database 2 | # have been loaded into bitcoind as watch addresses. 3 | # The bitcoind RPC for importing addresses does not return failure if an invalid account name argument is provided. 4 | # If any are missed then the order payments checks will fail to notice the payment. 5 | # This script can't simply check the address balance to test if address known to wallet 6 | # because getreceivedbyaddress returns 0 for addresses unknown to wallet. 7 | 8 | # If the BtcAddress model gets a failure code from bitcoind RPC, then it rolls back the create transaction so the database 9 | # *should* only hold addresses that were successfully added as watch addresses. 10 | 11 | if Market_bitcoinrpc.get_version >= 170000 12 | # RPC returns hash with keys being the addresses. 13 | watches = Market_bitcoinrpc.getaddressesbylabel(Rails.configuration.bitcoind_watch_address_label).keys 14 | else 15 | # Returns array. 16 | watches = Market_bitcoinrpc.getaddressesbyaccount(Rails.configuration.bitcoind_watch_address_label) 17 | end 18 | 19 | BtcAddress.bitcoin.unassigned.each do |b| 20 | unless watches.include?(b.address) 21 | raise "#{b.address} not found" 22 | end 23 | if Market_bitcoinrpc.getreceivedbyaddress(b.address) > 0 24 | # This address should have no transaction history before assigning to an order. 25 | raise "non-zero balance in #{b.address}" 26 | end 27 | end 28 | puts "Count of addresses in bitcoind: #{watches.size}" 29 | puts "Count of bitcoin addresses in market database: #{BtcAddress.bitcoin.count}" 30 | puts "Count of unassigned bitcoin market addresses: #{BtcAddress.bitcoin.unassigned.count}" 31 | 32 | exit unless PaymentMethod.litecoin 33 | 34 | # When litecoind RPC supports labels, this code will need to change like above. 35 | watches = Market_litecoinrpc.getaddressesbyaccount(Rails.configuration.bitcoind_watch_address_label) 36 | BtcAddress.litecoin.unassigned.each do |b| 37 | unless watches.include?(b.address) 38 | raise "#{b.address} not found" 39 | end 40 | if Market_litecoinrpc.getreceivedbyaddress(b.address) > 0 41 | raise "non-zero balance in #{b.address}" 42 | end 43 | end 44 | puts "Count of addresses in litecoind: #{watches.size}" 45 | puts "Count of litecoin addresses in market database: #{BtcAddress.litecoin.count}" 46 | puts "Count of unassigned litecoin market addresses: #{BtcAddress.litecoin.unassigned.count}" 47 | -------------------------------------------------------------------------------- /app/views/products/_indextable.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <% @products.each do |product| %> 3 | 4 | 5 | 11 | 36 | 39 | 40 | 41 | <% end %> 42 |
    6 | <% primary_image = product.image_set_array.first %> 7 | <% primary_image = 1 if primary_image.nil? %> 8 | <%# Displays a placeholder if no images are defined. %> 9 | <%= link_to image_tag(product.method("image#{primary_image}").call.url(:thumb), alt: 'product picture'), product %> 10 | 12 |

    <%= link_to product.title, product %>

    13 |
    Vendor: <%= link_to product.vendor.displayname, profile_path(product.vendor) %> <% concat("[on vacation]") if product.vendor.vacation %>
    14 | <%# Long strings like URLs in description will overflow outside their container, messing up formatting. 15 | # Normally this is an easy css fix but Firefox makes it difficult inside tables (read css file for more info). 16 | # Browsers seem to automatically break long strings on "-_" chars but if none present the string will overflow container. 17 | # Here it looks for long 60 char strings and wraps them in a div to apply word-breaking css. 18 | # The pattern needs to avoid matching any strings with html tags because it will break the html, 19 | # ie "

    xxxxxxxxxxxxxxxxxx yyyyyyy

    " would break p tag html if first non-whitespace string wrapped in div tags. 20 | #%> 21 | <% html = simple_format h( truncate(product.description, length: 400, separator: ' ') ) %> 22 | <% html.gsub!(/([^<>\-_\s]{60,})/, '
    \\1
    ') %> 23 | <%# rails automatically escapes html so this function will prevent escaping. It is already safe from calling h() above. %> 24 | <%=html.html_safe %> 25 | 26 | <% if product.fe_enabled %> 27 |
    Escrow: No
    28 | <%end%> 29 | 30 | <% if product.allow_purchase? %> 31 | 32 | <%else%> 33 | 34 | <%end%> 35 |
    37 | <%= render partial: 'products/unitprices', locals: {product: product, show_price_per_unit: false} %> 38 |
    43 | <%= paginate @products %> 44 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= Rails.configuration.sitename %> 5 | <%= stylesheet_link_tag "application", media: "all" %> 6 | <%# admin_controller? is a helper %> 7 | <%= stylesheet_link_tag("admin") if admin_controller? %> 8 | <%= csrf_meta_tags %> 9 | 10 | 11 | 12 |
    13 |
    14 |
    15 | <%= link_to image_tag(Rails.configuration.logo_filename, id: 'logo', alt: 'site logo', class: 'img-responsive'), root_path %> 16 |
    17 |
    18 |
    19 | <% if !current_user && !is_admin? && is_market? %> 20 | <%= link_to 'Login', new_session_path, class: "btn btn-default navbar-btn" %> 21 | <%= link_to 'Register', new_user_path, class: "btn btn-default navbar-btn" %> 22 | <% end %> 23 |
    24 |
    25 |
    26 |
    27 |
    28 | <% if current_user %> 29 | <%= render 'nav' %> 30 | <%= render 'categories' %> 31 | <%elsif is_admin? %> 32 | <%= render 'admin_nav' %> 33 | <% end %> 34 | 35 |
    <%# col-md3 %> 36 |
    37 |
    38 | <% flash.each do |name, msg| %> 39 | <% # map redirect_to flash keys (notice, alert) to bootstrap names. 40 | if name == 'notice' 41 | name = 'success' 42 | elsif name == 'alert' 43 | name = 'danger' 44 | else 45 | next # not interested in every flash key; some are for controlling logic ie newuser variable. 46 | end 47 | %> 48 | <%= content_tag :div, msg, :class => "alert alert-#{name}" %> 49 | <% end %> 50 |
    51 | 52 | <%= yield %> 53 | 54 | 55 | 56 |
    <%# col-md-9 %> 57 |
    <%# row %> 58 |
    <%# container %> 59 | 60 | 61 | <%# Randomize page sizes so fingerprinting by packet analysis is harder. Don't bother with admin pages. %> 62 | <% if Rails.configuration.random_pad_http_response && Rails.env.production? && !is_admin? %> 63 | 71 | <% end %> 72 | -------------------------------------------------------------------------------- /generate_address.rb: -------------------------------------------------------------------------------- 1 | require 'getoptlong' 2 | require 'json' 3 | 4 | def verbose(msg) 5 | if $verbose 6 | puts msg 7 | end 8 | end 9 | 10 | # If rails runner sees an argument it recognizes such as --help 11 | # it will use the argument for itself. Therefore to use the --help argument 12 | # on this script you need to call it like this: 13 | # rails r script.rb -- --help 14 | ARGV.shift if ARGV.first == '--' 15 | 16 | opts = GetoptLong.new( 17 | ['--test', GetoptLong::NO_ARGUMENT], 18 | ['--verbose', '-v', GetoptLong::NO_ARGUMENT], 19 | ['--address-type', '-a', GetoptLong::REQUIRED_ARGUMENT], 20 | ['--count', '-c', GetoptLong::OPTIONAL_ARGUMENT], 21 | ['--help', '-h', GetoptLong::NO_ARGUMENT], 22 | ) 23 | 24 | $verbose = false 25 | test_connectivity = false 26 | address_type = '' 27 | count = 1 28 | opts.each do |opt, arg| 29 | case opt 30 | when '--verbose' 31 | $verbose = true 32 | when '--test' 33 | test_connectivity = true 34 | when '--address-type' 35 | address_type = arg 36 | when '--count' 37 | count = arg.to_i 38 | when '--help' 39 | puts "#{$0} [--verbose] [--test] [--count n] --address-type " 40 | puts "\n --test : check connectivity to RPC and GPG" 41 | puts "\n --count n : number of addresses to generate" 42 | exit 43 | end 44 | end 45 | 46 | unless address_type[/BTC|LTC/] 47 | verbose("address-type must be BTC or LTC") 48 | exit 49 | end 50 | 51 | if test_connectivity 52 | 53 | verbose("new addresses will be assigned to [label] name: #{Rails.configuration.bitcoind_order_address_label}") 54 | 55 | if address_type == 'BTC' 56 | rpc = Payout_bitcoinrpc 57 | elsif address_type == 'LTC' 58 | rpc = Payout_litecoinrpc 59 | end 60 | info = rpc.getblockchaininfo # return ruby hash. 61 | if info.has_key?("blocks") 62 | verbose("RPC test success") 63 | else 64 | verbose("RPC test failed") 65 | end 66 | 67 | ga = GeneratedAddress.new 68 | ga.btc_address = 'test message' 69 | ga.sign 70 | if ga.pgp_signature[/BEGIN PGP SIGNED MESSAGE/] 71 | verbose("GPG signing test success") 72 | else 73 | verbose("GPG signing test failed") 74 | end 75 | exit 76 | end 77 | 78 | (1..count).each do 79 | ga = GeneratedAddress.new 80 | ga.address_type = address_type 81 | ga.generate # populate btc_address 82 | ga.sign # populate pgp_signature 83 | if ga.save 84 | ScriptLog.info("generated [#{ga.address_type}] address: #{ga.btc_address}") 85 | else 86 | ScriptLog.info "failed to save [#{ga.btc_address}]" 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /app/jobs/update_market_order_payouts_job.rb: -------------------------------------------------------------------------------- 1 | require 'socksify/http' 2 | 3 | class UpdateMarketOrderPayoutsJob < ApplicationJob 4 | queue_as :default 5 | 6 | # This job runs only on the payout server. 7 | # Called by the process payouts job to update market with txids of payments. 8 | # Can be called manually if a previous run failed to connect to market. ie rails r "UpdateMarketOrderPayoutsJob.perform_now" 9 | # Builds json http request and calls the set_paid api on the market. 10 | # When txids on payout server change due to bumpfee, this will update market with new txids. PayoutConfirmationsJob needs to 11 | # run and it will detect transactions that have had their fee bumped and replace the txid field in local database and set 12 | # market_updated to false so that this job will update market. Note feebumping is not done in this app; bitcoind RPC must be 13 | # called externally. 14 | # Note, the update_all ActiveRecord method called here does not change the updated_at timestamp on the local payouts. 15 | def perform 16 | payouts = Payout.where(market_updated: false).where(paid: true) 17 | count = payouts.size 18 | if count == 0 19 | ScriptLog.info("All paid Payouts have already had their corresponding OrderPayout updated on the market. Nothing to do.") 20 | return 21 | end 22 | if payouts.where(txid: nil).count > 0 23 | ScriptLog.info("A payout is missing the txid field. aborting.") 24 | return 25 | end 26 | 27 | ScriptLog.info("updating #{count} OrderPayouts on market") 28 | 29 | update_hash = {} 30 | payouts.each do |p| 31 | update_hash[p.order_payout_id] = { paid: true, txid: p.txid } 32 | end 33 | 34 | uri = URI.parse(Rails.configuration.admin_api_uri_base + 35 | '/admin/orderpayouts/set_paid') 36 | http = nil 37 | if Rails.env.production? 38 | tor_proxy_host = Rails.configuration.tor_proxy_host 39 | tor_proxy_port = Rails.configuration.tor_proxy_port 40 | http = Net::HTTP.SOCKSProxy(tor_proxy_host, tor_proxy_port).new(uri.host, uri.port) 41 | else 42 | http = Net::HTTP.new(uri.host, uri.port) 43 | end 44 | 45 | request = Net::HTTP::Post.new(uri.request_uri) 46 | request.content_type = 'application/json' 47 | request.body = { 48 | admin_api_key: Rails.configuration.admin_api_key, 49 | changes: update_hash, 50 | }.to_json 51 | response = http.request(request) 52 | r = JSON.parse response.body # generate exception if json not returned 53 | if r['api_return_code'] == 'success' 54 | payouts.update_all(market_updated: true) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/jobs/payout_confirmations_job.rb: -------------------------------------------------------------------------------- 1 | # Since this job is only run when admin views payout index, it is possible that a payouts transaction is no longer 2 | # in the mempool when this runs. In that case getrawtransaction will fail and fee , vsize will not be saved. 3 | # To avoid that problem, schedule this job daily, or recode to save_fee after processing payments instead of after being confirmed. Or stop using prune option. 4 | class PayoutConfirmationsJob < ApplicationJob 5 | queue_as :default 6 | 7 | def save_fee(payout, rpc) 8 | begin 9 | tx = rpc.gettransaction(payout.txid) 10 | fee_in_satoshi = tx['fee'].abs * 100000000 11 | # This only works for transactions in the mempool if you don't have -txindex set. 12 | tx = rpc.getrawtransaction(payout.txid, true) 13 | vsize = tx['vsize'] 14 | payout.update!(fee: fee_in_satoshi, vsize: vsize) 15 | rescue BitcoinRPC::JSONRPCError 16 | ScriptLog.error "rpc unable to get info to calculate fee on #{payout.txid}" 17 | end 18 | end 19 | 20 | def perform 21 | ScriptLog.info "Looking unconfirmed payouts to check if they are now confirmed." 22 | payouts = Payout.where(paid: true, confirmed: false) 23 | payouts_count = payouts.count 24 | if payouts_count > 0 25 | ScriptLog.info "Found #{payouts_count} payouts to check." 26 | end 27 | payouts.each do |payout| 28 | if payout.address_type == 'BTC' 29 | rpc = Payout_bitcoinrpc 30 | elsif payout.address_type == 'LTC' 31 | rpc = Payout_litecoinrpc 32 | end 33 | tx = rpc.gettransaction(payout.txid) 34 | save_fee(payout, rpc) if payout.fee.nil? 35 | if tx['confirmations'] > 0 36 | if payout.update(confirmed: true) 37 | ScriptLog.info "payout id: #{payout.id}, txid: #{payout.txid} is now confirmed." 38 | else 39 | ScriptLog.error "payout id: #{payout.id} error updating payout confirmed:true." 40 | end 41 | elsif tx.has_key? 'replaced_by_txid' 42 | tx_new = rpc.gettransaction(tx['replaced_by_txid']) 43 | # Update txid and force update_market_order_payouts.rb to re-update market. 44 | payout.update(txid: tx['replaced_by_txid'], market_updated: false) 45 | save_fee(payout, rpc) 46 | if tx_new['confirmations'] > 0 47 | if payout.update(confirmed: true) 48 | ScriptLog.info "payout id: #{payout.id}, txid: #{payout.txid} is now confirmed. txid was replaced." 49 | else 50 | ScriptLog.error "payout id: #{payout.id} error updating payout confirmed:true." 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/controllers/admin/messages_controller.rb: -------------------------------------------------------------------------------- 1 | # Mostly copied from MessagesController with much removed since we are only viewing. 2 | class Admin::MessagesController < ApplicationController 3 | before_action :require_admin 4 | 5 | # List of all conversations the specified user had with other parties. 6 | # Every interaction with another user is a converstation. There is only one conversation thread with each other user (very simple). 7 | def conversations 8 | # GET /messages/:id 9 | @user = User.find(params[:id]) 10 | # Array of MessageRef objects but due to GROUP BY they only have attributes otherparty_id, newest_message_date, cnt, unseen_cnt. 11 | @conversations = MessageRef.select('otherparty_id, max(created_at) newest_message_date, count(1) as cnt, sum(unseen) as unseen_cnt'). 12 | group('otherparty_id'). 13 | where(user: @user). 14 | order('unseen_cnt desc, newest_message_date desc') # Primary sort groups with unread messages so they appear at top. 15 | 16 | # Additional list is generated to help with seeing deleted messages (refs). 17 | all_recipients = Message.select('recipient_id').where(sender: @user).group('recipient_id').unscope(:order).collect{|row| User.find(row['recipient_id']) } 18 | all_senders = Message.select('sender_id').where(recipient: @user).group('sender_id').unscope(:order).collect{|row| User.find(row['sender_id']) } 19 | @parties = all_recipients | all_senders 20 | end 21 | 22 | def show_conversation 23 | # GET /messages/:id/:otherparty_id 24 | @user = User.find(params[:id]) 25 | @otherparty = User.find(params[:otherparty_id]) 26 | 27 | # This is a ruby group_by not SQL GROUP BY. 28 | # Returns hash where keys are string from strftime and value is an array of messages that have same value 29 | # so messages grouped by day they were created. We sort the hash keys in the view. 30 | @message_daygroups = @user.message_refs.where(otherparty: @otherparty).group_by do |msg| 31 | msg.created_at.in_time_zone(admin_user.timezone).strftime('%F') 32 | end 33 | 34 | # undeleted_messages is the list of messages that have references pointing to them. 35 | undeleted_messages = @user.message_refs.where(otherparty: @otherparty).collect{|msg_ref| msg_ref.message } 36 | all_messages = Message.where(sender: [@user, @otherparty]).where(recipient: [@user, @otherparty]) 37 | @deleted_messages = all_messages - undeleted_messages 38 | @deleted_message_daygroups = @deleted_messages.group_by do |msg| 39 | msg.created_at.in_time_zone(admin_user.timezone).strftime('%F') 40 | end 41 | end 42 | 43 | 44 | end 45 | -------------------------------------------------------------------------------- /app/views/messages/_conversation.html.erb: -------------------------------------------------------------------------------- 1 | <%# Originally timestamps were not displayed to increase security of traffic correlation. Then decided that displaying minute, not seconds mitigates the risk somewhat. 2 | However, haven't found a nice way to display the timestamps without making message list ugly. So make timestamps available in source html only. %> 3 | 4 | <% encrypted_messages = {} %> 5 | <%# sort hash by keys %> 6 | <% @message_daygroups.sort.reverse.each do |day, message_refs| %> 7 |
    8 |
    <%= day.in_time_zone(current_user.timezone).strftime('%F') %>
    9 | <% message_refs.sort_by{|msg| msg.created_at}.reverse.each do |message_ref| %> 10 | <% cssclass = [] %> 11 | <% cssclass.push message_ref.direction == 'sent' ? 'sent' : 'received' %> 12 | <% if message_ref.message.body[/-----BEGIN/] %> 13 | <% cssclass.push 'encrypted' %> 14 | <% else %> 15 | <% cssclass.push 'plaintext' %> 16 | <% end %> 17 |
    <%=message_ref.created_at.in_time_zone(current_user.timezone).strftime('%H:%M') %>
    18 |
    19 | <%= message_ref.message.body.split("\r\n").collect{ |line| h line}.join("
    ").html_safe %> 20 | <% if cssclass.include?('encrypted') %> 21 |
    <%= link_to("Copy", "#pgp_message_copy_#{message_ref.id}") %>
    22 | <% encrypted_messages[message_ref.id] = message_ref.message.body %> 23 | <%end%> 24 |
    25 | <%# The above message is printed on one long line in the source code with no newline chars. 26 | This allows copying PGP messages correctly while using the break-word styling in Firefox. 27 | Chrome doesn't have the copy problem that Firefox has. %> 28 | <% end %> 29 |
    <%# daygroup %> 30 | 31 | <% end %> 32 | 33 | <%# These modals rendered at bottom of page instead of loop above just to make code and html tidier %> 34 | <% encrypted_messages.each do |id, body| %> 35 | 50 | <%end%> 51 | -------------------------------------------------------------------------------- /app/controllers/vendor/order_payouts_controller.rb: -------------------------------------------------------------------------------- 1 | class Vendor::OrderPayoutsController < ApplicationController 2 | before_action :require_vendor 3 | 4 | def index 5 | # To find bitcoin order_payouts, need to join orders (for payment_method_id) then join payment_methods to access payment method code. 6 | # This is a similar query to one used in admin/order_payouts_api_controller.rb. 7 | @btc_order_payouts = OrderPayout.joins(order: [:payment_method]).joins(:user). 8 | select('order_payouts.order_id, orders.created_at as order_created, orders.title, order_payouts.txid IS NULL as txid_missing, 9 | orders.btc_price, order_payouts.btc_address, order_payouts.btc_amount, orders.commission, order_payouts.txid, order_payouts.updated_at'). 10 | where(paid: true).where(payment_methods: {code: "BTC"}).where(user: current_user).where(orders: {deleted_by_vendor: false}). 11 | order('txid_missing, order_payouts.updated_at DESC') 12 | 13 | # Ensure conditions are identical to above query. 14 | @btc_summary = OrderPayout.joins(order: [:payment_method]).joins(:user). 15 | select('SUM(orders.btc_price) as total_orders, SUM(orders.commission) as total_commissions, SUM(order_payouts.btc_amount) as total_payouts'). 16 | where(paid: true).where(payment_methods: {code: "BTC"}).where(user: current_user).where(orders: {deleted_by_vendor: false}) 17 | 18 | if PaymentMethod.litecoin_exists? 19 | 20 | @ltc_order_payouts = OrderPayout.joins(order: [:payment_method]).joins(:user). 21 | select('order_payouts.order_id, orders.created_at as order_created, orders.title, order_payouts.txid IS NULL as txid_missing, 22 | orders.btc_price, order_payouts.btc_address, order_payouts.btc_amount, orders.commission, order_payouts.txid, order_payouts.updated_at'). 23 | where(paid: true).where(payment_methods: {code: "LTC"}).where(user: current_user).where(orders: {deleted_by_vendor: false}). 24 | order('txid_missing, order_payouts.updated_at DESC') 25 | 26 | # Ensure conditions are identical to above query. 27 | @ltc_summary = OrderPayout.joins(order: [:payment_method]).joins(:user). 28 | select('SUM(orders.btc_price) as total_orders, SUM(orders.commission) as total_commissions, SUM(order_payouts.btc_amount) as total_payouts'). 29 | where(paid: true).where(payment_methods: {code: "LTC"}).where(user: current_user).where(orders: {deleted_by_vendor: false}) 30 | 31 | end 32 | 33 | @revenue = current_user.revenue(current_user.currency) 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /config/locales/humanizer.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | humanizer: 3 | validation: 4 | error: is not correct 5 | questions: 6 | - question: Two plus two? 7 | answers: ["4", "four"] 8 | - question: Jack and Jill went up the... 9 | answer: hill 10 | - question: What is the number before twelve? 11 | answers: ["11", "eleven"] 12 | - question: Five times two is what? 13 | answers: ["10", "ten"] 14 | - question: "Insert the next number in this sequence: 10, 11, 12, 13, 14, .." 15 | answers: ["15", "fifteen"] 16 | - question: "What is five times five?" 17 | answers: ["25", "twenty-five"] 18 | - question: "Ten divided by two is what?" 19 | answers: ["5", "five"] 20 | - question: What day comes after Monday? 21 | answer: "tuesday" 22 | - question: What is the last month of the year? 23 | answer: "december" 24 | - question: How many minutes are in an hour? 25 | answers: ["60", "sixty"] 26 | - question: What is the opposite of down? 27 | answer: "up" 28 | - question: What is the opposite of north? 29 | answer: "south" 30 | - question: What is the opposite of bad? 31 | answer: "good" 32 | - question: What is 4 times four? 33 | answers: ["16", "sixteen"] 34 | - question: What number comes after 20? 35 | answers: ["21", "twenty one"] 36 | - question: What month comes before July? 37 | answer: "june" 38 | - question: What is fifteen divided by three? 39 | answer: ["five", "5"] 40 | - question: What is one hundred divided by one? 41 | answer: ["100", "one hundred"] 42 | - question: What is 14 minus 4? 43 | answers: ["10", "ten"] 44 | - question: "What comes next? Monday, Tuesday, Wednesday.." 45 | answer: "Thursday" 46 | - question: "Number of days in a week?" 47 | answers: ["7", "seven"] 48 | - question: "Five plus 3 plus 2 ?" 49 | answers: ["10", "ten"] 50 | - question: The country bordering western Spain? 51 | answer: portugal 52 | - question: The country bordering south USA? 53 | answer: mexico 54 | - question: The country bordering north USA? 55 | answer: canada 56 | - question: Saskatoon is in what country? 57 | answer: canada 58 | - question: Three plus 3 is? 59 | answers: ["6", "six"] 60 | - question: In what country is the city Amsterdam? 61 | answer: netherlands 62 | - question: The compass points to ... 63 | answer: north 64 | - question: The earth orbits the ... 65 | answer: sun 66 | - question: What is the opposite of left? 67 | answer: right 68 | -------------------------------------------------------------------------------- /app/views/messages/conversations.html.erb: -------------------------------------------------------------------------------- 1 |

    Messages

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% @conversations.each do |conversation| %> 17 | 18 | 19 | 26 | 27 | 28 | <%# unseen is only set on 'received' message refs (see controller create method). 29 | It would be more efficient to use a single query in controller rather than in this loop but there shouldn't be huge numbers of conversations (Todo).%> 30 | 36 | 40 | 41 | <% end %> 42 | 43 |
    <%= current_user.vendor ? "Buyer name" : "Vendor name" %>New messagesMessage countLast sent/receivedRecipient seen
    <%= link_to conversation.otherparty.displayname, profile_path(conversation.otherparty) %> 20 | <% if conversation.unseen_cnt > 0 %> 21 | <%= link_to "#{conversation.unseen_cnt} new", show_conversation_path(conversation.otherparty), class: 'btn btn-success' %> 22 | <%else%> 23 | <%= conversation.unseen_cnt %> 24 | <%end%> 25 | <%= conversation.cnt %><%= conversation.newest_message_date.in_time_zone(current_user.timezone).strftime('%F') %><%if conversation.sent_anything == false %> 31 | n/a 32 | <%else%> 33 | <%=image_tag('checkmark.svg') unless MessageRef.where(user: conversation.otherparty, otherparty: current_user, direction: 'received', unseen: 1).count > 0 %> 34 | <%end%> 35 | 37 | <%= link_to 'View', show_conversation_path(conversation.otherparty), class: 'btn btn-primary' %> 38 | <%= link_to 'Delete', "#delete_#{conversation.otherparty.id}", class: 'btn btn-danger' %> 39 |
    44 |

    45 | To send a message, visit the user's profile and click the send message link. 46 |

    47 | 48 | <% @conversations.each do |conversation| %> 49 | 63 | <% end %> 64 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require 'spec_helper' 4 | require File.expand_path("../../config/environment", __FILE__) 5 | require 'rspec/rails' 6 | # Add additional requires below this line. Rails is not loaded until this point! 7 | 8 | # Requires supporting ruby files with custom matchers and macros, etc, in 9 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 10 | # run as spec files by default. This means that files in spec/support that end 11 | # in _spec.rb will both be required and run as specs, causing the specs to be 12 | # run twice. It is recommended that you do not name files matching this glob to 13 | # end with _spec.rb. You can configure this pattern with the --pattern 14 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 15 | # 16 | # The following line is provided for convenience purposes. It has the downside 17 | # of increasing the boot-up time by auto-requiring all files in the support 18 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 19 | # require only the support files necessary. 20 | # 21 | # Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 22 | 23 | # Checks for pending migrations before tests are run. 24 | # If you are not using ActiveRecord, you can remove this line. 25 | ActiveRecord::Migration.maintain_test_schema! 26 | 27 | RSpec.configure do |config| 28 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 29 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 30 | 31 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 32 | # examples within a transaction, remove the following line or assign false 33 | # instead of true. 34 | config.use_transactional_fixtures = true 35 | 36 | # RSpec Rails can automatically mix in different behaviours to your tests 37 | # based on their file location, for example enabling you to call `get` and 38 | # `post` in specs under `spec/controllers`. 39 | # 40 | # You can disable this behaviour by removing the line below, and instead 41 | # explicitly tag your specs with their type, e.g.: 42 | # 43 | # RSpec.describe UsersController, :type => :controller do 44 | # # ... 45 | # end 46 | # 47 | # The different available types are documented in the features, such as in 48 | # https://relishapp.com/rspec/rspec-rails/docs 49 | config.infer_spec_type_from_file_location! 50 | end 51 | 52 | # After making modal dialogs have hidden html tag, capybara couldn't click buttons on them so changed this setting. 53 | # They have hidden tag because firefox will briefly show the modals before css has loaded. 54 | Capybara.ignore_hidden_elements = false 55 | --------------------------------------------------------------------------------