diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..277f336 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM starefossen/ruby-node:2-8 + +RUN mkdir /matreon +WORKDIR /matreon + +RUN apt-get update -qq && apt-get install -y build-essential libpq-dev --no-install-recommends && rm -rf /var/lib/apt/lists/* + +ENV RAILS_ENV production +ENV RAILS_SERVE_STATIC_FILES true +ENV RAILS_LOG_TO_STDOUT true + +COPY Gemfile /matreon/ +COPY Gemfile.lock /matreon/ +RUN bundle config --global frozen 1 +RUN bundle install --without development test + +ENV NODE_ENV production + +COPY package.json /matreon/package.json +COPY yarn.lock /matreon/yarn.lock +RUN yarn install + +COPY Procfile Rakefile config.ru .babelrc .postcssrc.yml /matreon/ +COPY config /matreon/config +COPY db /matreon/db +COPY vendor /matreon/vendor +COPY app /matreon/app +COPY bin /matreon/bin +COPY lib /matreon/lib + +COPY public /matreon/public +RUN rm -rf public/assets public/packs + +ENV DEVISE_SECRET_KEY ${DEVISE_SECRET_KEY} +RUN bundle exec rake assets:precompile + +EXPOSE 3000 +CMD bundle exec puma -C config/puma.rb diff --git a/Gemfile b/Gemfile index b338ffd..cd71dd3 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,9 @@ gem 'react_on_rails', '10.1.3' # Use Puma as the app server gem 'puma', '~> 3.11' -# Use SCSS for stylesheets + +# Proxy Lightning Charge: +gem 'rack-proxy' gem 'secure_headers', '~> 6.0.0.alpha02' diff --git a/Gemfile.lock b/Gemfile.lock index 0d17a68..b166ae1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -372,6 +372,7 @@ DEPENDENCIES mini_racer pg puma (~> 3.11) + rack-proxy rack-timeout rails (~> 5.1.6) rails-controller-testing diff --git a/Matreon.Template b/Matreon.Template new file mode 100644 index 0000000..d48432d --- /dev/null +++ b/Matreon.Template @@ -0,0 +1,414 @@ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + + "Description" : "Creates a single EC2 instance with a pruned Bitcoin Core node, C-Lightning, Lightning Charge, Postgres, Ruby on Rails and Matreon.", + + "Metadata" : { + "AWS::CloudFormation::Interface" : { + "ParameterGroups" : [ + { + "Label" : { "default" : "Server" }, + "Parameters" : [ "InstanceType", "KeyName", "SSHLocation" , "BugsEmail" ] + }, + { + "Label" : { "default":"Bitcoin" }, + "Parameters" : [ "Network" ] + }, + { + "Label" : { "default":"Matreon" }, + "Parameters" : [ "HostName", "FromEmail", "SmtpHost", "SmtpUser", "SmtpPassword", "SmtpPort" ] + }, + { + "Label" : { "default":"Podcast" }, + "Parameters" : [ "Podcast", "PodcastTitle", "PodcastImage", "PodcastUrl" ] + } + ] + }, + }, + + "Parameters" : { + + "KeyName": { + "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances", + "Type": "AWS::EC2::KeyPair::KeyName", + "ConstraintDescription" : "must be the name of an existing EC2 KeyPair." + }, + + "Network" : { + "Description" : "Real Bitcoin or testnet", + "Type" : "String", + "Default" : "testnet", + "AllowedValues" : [ "testnet", "bitcoin"] + }, + + "InstanceType" : { + "Description" : "WebServer EC2 instance type. Unless you are famous t2.small should suffice", + "Type" : "String", + "Default" : "t2.small", + "AllowedValues" : [ "t2.small", "t2.medium", "t2.large", "t2.large", "t2.xlarge", "t2.2xlarge"], + "ConstraintDescription" : "must be a valid EC2 instance type." + }, + + "SSHLocation" : { + "Description" : "The IP address range that can be used to SSH to the EC2 instances", + "Type": "String", + "MinLength": "9", + "MaxLength": "18", + "Default": "0.0.0.0/0", + "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", + "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." + }, + + "HostName": { + "Default": "http://example.com", + "Description" : "Hostname, no trailing slash", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "FromEmail": { + "Default": "you@example.com", + "Description" : "From email address", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "BugsEmail": { + "Default": "bugs@example.com", + "Description" : "Bug report email address", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "SmtpHost": { + "Default": "smtp.fastmail.com", + "Description" : "SMTP server", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "SmtpPort": { + "Default": "587", + "Description" : "SMTP port", + "Type": "String", + "MinLength": "1", + "MaxLength": "5" + }, + + "SmtpUser": { + "Default": "you@example.com", + "Description" : "SMTP username", + "Type": "String", + "MinLength": "5", + "MaxLength": "100" + }, + + "SmtpPassword": { + "Default": "", + "Description" : "SMTP password", + "Type": "String", + "NoEcho" : "true", + "MinLength": "0", + "MaxLength": "100" + }, + + "Podcast" : { + "Description" : "Enable podcast feature. Currently requires an existing RSS source.", + "Type" : "String", + "Default" : "0", + "AllowedValues" : [ "0", "1"] + }, + + "PodcastTitle" : { + "Description" : "Podcast title", + "Type": "String", + "MinLength": "0", + "MaxLength": "100" + }, + + "PodcastImage" : { + "Description" : "Podcast image URL", + "Type": "String", + "MinLength": "0", + "MaxLength": "255" + }, + + "PodcastUrl" : { + "Description" : "Existing podcast RSS feed", + "Type": "String", + "MinLength": "0", + "MaxLength": "255" + } + + }, + + "Resources" : { + + "WebServer": { + "Type": "AWS::EC2::Instance", + "Metadata" : { + "AWS::CloudFormation::Init" : { + "configSets" : { + "full_install" : [ + "install_cfn", + "install_docker", + "prepare_datadir", + "install_container_bitcoin", + "install_container_lightning", + "install_container_charge", + "install_docker_compose_with_pg_and_rails", + "initial_blockchain_download", + "launch_matreon" + ] + }, + + "install_cfn" : { + "files" : { + "/etc/cfn/cfn-hup.conf" : { + "content" : { "Fn::Join" : ["", [ + "[main]\n", + "stack=", { "Ref" : "AWS::StackId" }, "\n", + "region=", { "Ref" : "AWS::Region" }, "\n" + ]]}, + "mode" : "000400", + "owner" : "root", + "group" : "root" + }, + + "/etc/cfn/hooks.d/cfn-auto-reloader.conf" : { + "content": { "Fn::Join" : ["", [ + "[cfn-auto-reloader-hook]\n", + "triggers=post.update\n", + "path=Resources.WebServer.Metadata.AWS::CloudFormation::Init\n", + "action=/opt/aws/bin/cfn-init -v ", + " --stack ", { "Ref" : "AWS::StackName" }, + " --resource WebServer ", + " --configsets full_install ", + " --region ", { "Ref" : "AWS::Region" }, "\n", + "runas=root\n" + ]]}, + "mode" : "000400", + "owner" : "root", + "group" : "root" + } + }, + + "services" : { + "sysvinit" : { + "cfn-hup" : { "enabled" : "true", "ensureRunning" : "true", + "files" : ["/etc/cfn/cfn-hup.conf", "/etc/cfn/hooks.d/cfn-auto-reloader.conf"]} + } + } + }, + + "install_docker": { + "commands": { + "01_install_docker": { + "command": "yum install -y docker git" + }, + "02_install_docker_compose": { + "command": {"Fn::Join" : ["", [ + "curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose &&", + "chmod +x /usr/local/bin/docker-compose &&", + "ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose &&", + "chmod +x /usr/bin/docker-compose" + ]]} + }, + "03_start_service": { + "command": "service docker start" + } + } + }, + + "prepare_datadir": { + "commands": { + "01_create_datadir": { + "command": "mkdir /matreon_data" + }, + "02_set_env": { + "command": { "Fn::Join" : ["", [ + "echo \"SECRET_KEY_BASE=`hexdump -n 64 -e '16/4 \"%08x\" 1 \"\n\"' /dev/random`\" >> /matreon_data/.env \n", + "echo \"DEVISE_SECRET_KEY=`hexdump -n 64 -e '16/4 \"%08x\" 1 \"\n\"' /dev/random`\" >> /matreon_data/.env \n", + "echo \"LIGHTNING_CHARGE_API_TOKEN=`hexdump -n 64 -e '16/4 \"%08x\" 1 \"\n\"' /dev/random`\" >> /matreon_data/.env \n", + "echo \"HOSTNAME=", { "Ref" : "HostName" }, "\" >> /matreon_data/.env \n", + "echo \"FROM_EMAIL=", { "Ref" : "FromEmail" }, "\" >> /matreon_data/.env \n", + "echo \"BUGS_TO=", { "Ref" : "BugsEmail" }, "\" >> /matreon_data/.env \n", + "echo \"SMTP_HOST=", { "Ref" : "SmtpHost" }, "\" >> /matreon_data/.env \n", + "echo \"SMTP_PORT=", { "Ref" : "SmtpPort" }, "\" >> /matreon_data/.env \n", + "echo \"SMTP_USERNAME=", { "Ref" : "SmtpUser" }, "\" >> /matreon_data/.env \n", + "echo \"SMTP_PASSWORD=", { "Ref" : "SmtpPassword" }, "\" >> /matreon_data/.env \n", + "echo \"NETWORK=", { "Ref" : "Network" }, "\" >> /matreon_data/.env \n", + "echo \"PODCAST=", { "Ref" : "Podcast" }, "\" >> /matreon_data/.env \n", + "echo \"PODCAST_TITLE=", { "Ref" : "PodcastTitle" }, "\" >> /matreon_data/.env \n", + "echo \"PODCAST_URL=", { "Ref" : "PodcastUrl" }, "\" >> /matreon_data/.env \n", + "echo \"PODCAST_IMAGE=", { "Ref" : "PodcastImage" }, "\" >> /matreon_data/.env \n", + "echo \"DATADIR=/matreon_data", "\" >> /matreon_data/.env \n", + "echo \"IP_ADDRESS=", { "Ref" : "IPAddress" }, "\" >> /matreon_data/.env\n", + "source /matreon_data/.env" + ]]} + } + } + }, + + "install_container_bitcoin": { + "commands": { + "01_configure_datadir": { + "command": "mkdir /matreon_data/bitcoin" + }, + "02_clone_repo": { + "command": "git clone https://github.com/NicolasDorier/docker-bitcoin /usr/local/src/docker-bitcoin" + }, + "03_docker_build": { + "command": "docker build /usr/local/src/docker-bitcoin/core/0.16.0 -t bitcoind:0.16.0" + } + } + }, + + "install_container_lightning": { + "commands": { + "01_configure_datadir": { + "command": "mkdir /matreon_data/lightning" + }, + "02_clone_repo": { + "command": "git clone https://github.com/cdecker/dockerfiles /usr/local/src/docker-lightning" + }, + "03_docker_build": { + "command": "docker build /usr/local/src/docker-lightning/lightning/node -f /usr/local/src/docker-lightning/lightning/node/Dockerfile.master -t lightningd:latest" + } + } + }, + + "install_container_charge": { + "commands": { + "01_configure_datadir": { + "command": "mkdir /matreon_data/charge" + }, + "02_clone_repo": { + "command": "git clone https://github.com/Sjors/lightning-charge /usr/local/src/lightning-charge && cd /usr/local/src/lightning-charge && git checkout 2018/05/node-uri" + }, + "03_docker_build": { + "command": "docker build /usr/local/src/lightning-charge -t charge:latest" + } + } + }, + + "install_docker_compose_with_pg_and_rails": { + "commands": { + "01_make_db_dir": { + "command": "mkdir /matreon_data/pg" + }, + "02_clone_repo": { + "command": "git clone https://github.com/Sjors/matreon.git /usr/local/src/matreon && cd /usr/local/src/matreon && git checkout 2018/05/docker-compose-aws-cloud" + }, + "03_build_rails_container": { + "command": "cd /usr/local/src/matreon && docker-compose build" + } + } + }, + + "initial_blockchain_download": { + "commands": { + "01_start_bitcoind": { + "command": "cd /usr/local/src/matreon && set -a && source /matreon_data/.env && docker-compose up -d bitcoind" + }, + "02_wait_for_sync": { + "command": "cd /usr/local/src/matreon && set -a && source /matreon_data/.env && ./wait_for_ibd.sh > /var/log/wait_for_ibd.log" + } + } + }, + + "launch_matreon": { + "01_launch": { + "command": "cd /usr/local/src/matreon && set -a && source /matreon_data/.env && docker-compose up -d" + }, + "02_migrate_db": { + "command": "cd /usr/local/src/matreon && docker-compose run web rake db:migrate" + }, + "03_install_crontab": { + "command": { "Fn::Join" : ["", [ + "echo '0 * * * * root cd /usr/local/src/matreon && set -a && source /matreon_data/.env && docker-compose run web rake invoices:process' >> /tmp/crontab \n", + "echo '0 * * * * root cd /usr/local/src/matreon && set -a && source /matreon_data/.env && docker-compose run web rake podcast:fetch' >> /tmp/crontab \n", + "echo '' >> /tmp/crontab", + "crontab /tmp/crontab" + ]]} + } + } + } + }, + "Properties": { + "ImageId" : "ami-5652ce39", + "InstanceType" : { "Ref" : "InstanceType" }, + "BlockDeviceMappings" : [ + { + "DeviceName" : "/dev/xvda", + "Ebs" : { + "VolumeSize" : "25" + } + } + ], + "SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ], + "KeyName" : { "Ref" : "KeyName" }, + "Tags" : [ + {"Key" : "Name", "Value" : "Matreon"} + ], + "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ + "#!/bin/bash -xe\n", + "yum update -y aws-cfn-bootstrap\n", + + "/opt/aws/bin/cfn-init -v ", + " --stack ", { "Ref" : "AWS::StackId" }, + " --resource WebServer ", + " --configsets full_install ", + " --region ", { "Ref" : "AWS::Region" }, "\n", + + "/opt/aws/bin/cfn-signal -e $? ", + " --stack ", { "Ref" : "AWS::StackId" }, + " --resource WebServer ", + " --region ", { "Ref" : "AWS::Region" }, "\n" + ]]}} + }, + "CreationPolicy" : { + "ResourceSignal" : { + "Timeout" : "PT14400M" + } + } + }, + + "IPAddress" : { + "Type" : "AWS::EC2::EIP" + }, + + "IPAssoc" : { + "Type" : "AWS::EC2::EIPAssociation", + "Properties" : { + "InstanceId" : { "Ref" : "WebServer" }, + "EIP" : { "Ref" : "IPAddress" } + } + }, + + "WebServerSecurityGroup" : { + "Type" : "AWS::EC2::SecurityGroup", + "Properties" : { + "GroupDescription" : "Enable SSH, Bitcoin P2P, Lightning P2P and Charge access", + "SecurityGroupIngress" : [ + {"IpProtocol" : "tcp", "FromPort" : "8883", "ToPort" : "8883", "CidrIp" : "0.0.0.0/0"}, + {"IpProtocol" : "tcp", "FromPort" : "18883", "ToPort" : "18883", "CidrIp" : "0.0.0.0/0"}, + {"IpProtocol" : "tcp", "FromPort" : "9735", "ToPort" : "9735", "CidrIp" : "0.0.0.0/0"}, + {"IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"}, + {"IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "SSHLocation"}} + ] + } + } + }, + + "Outputs" : { + "WebsiteURL" : { + "Value" : { "Fn::Join" : ["", ["http://", { "Fn::GetAtt" : [ "WebServer", "PublicDnsName" ]}, "/" ]] }, + "Description" : "URL for your Matreon" + } + } +} diff --git a/README.md b/README.md index 5507ee7..fbae18a 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,144 @@ Current status: extremely experimental! Live instance: [matreon.sprovoost.nl](https://matreon.sprovoost.nl/) -## Prerequisites +## Deploy to AWS using Docker -You need to run [c-lightning](https://github.com/ElementsProject/lightning) and [Lightning Charge](https://github.com/ElementsProject/lightning-charge) somewhere. +This is currently quite brittle and not very secure. + +Install the Amazon CloudFormation template by downloading [Matreon.Template](https://raw.githubusercontent.com/Sjors/matreon/master/Matreon.Template) and then uploading it on the [CloudFormation stack creation page](https://eu-central-1.console.aws.amazon.com/cloudformation/home?region=eu-central-1&stackName=Matreon#/stacks/new). + +Fill out the form, click next a few times and then wait... This [blog post](https://medium.com/provoost-on-crypto/bitcoin-core-lightning-rails-on-aws-ad3bd45b11e0) explains the steps in more detail. + +## Deploy elsewhere using Docker + +Install [Docker](https://docs.docker.com/install/). + +Create a directory to store the blockchain, wallet info, etc: + +```sh +mkdir matreon-vol +mkdir matreon-vol/bitcoin +mkdir matreon-vol/lightning +mkdir matreon-vol/charge +mkdir matreon-vol/pg +``` + +We use Docker Compose to combine a number of containers. Hardcoding a [Docker image checksum](https://docs.docker.com/engine/security/trust/content_trust/#content-trust-operations-and-keys) doesn't prove how the image was built, so to minimize trust, we have to build the containers locally. + +### Container 1 - Bitcoin Core + +```sh +git clone https://github.com/NicolasDorier/docker-bitcoin +docker build docker-bitcoin/core/0.16.0 -t bitcoind:0.16.0 +``` + +Once the container is running (see below), you can view bitcoind logs (e.g. to + see how syncing the blockchain is going): + +``` +docker logs -f --since 1m matreon_bitcoind_1 +``` + + +### Container 2 - C-Lightning + +```sh +git clone https://github.com/cdecker/dockerfiles docker-lightning +docker build docker-lightning/lightning/node -f docker-lightning/lightning/node/Dockerfile.master -t lightningd:latest +``` + +Once the container is running (see below), you can monitor the logs: + +```sh +docker logs -f --since 1m matreon_lightningd_1 +``` + +Or interact: + +```sh +docker-compose exec lightningd lightning-cli help +``` + +### Container 3 - Lightning Charge + +```sh +git clone https://github.com/ElementsProject/lightning-charge +docker build lightning-charge -t charge:latest +``` + +### Container 4 - Postgres + +We're trusting the upstream image for now. + +### Container 5 - Rails & Matreon + +```sh +git clone https://github.com/Sjors/matreon.git +``` + +Once the container is running (see below), you can open a Rails console: + +```sh +docker-compose run web rails console +``` + +Or view the server logs: + +```sh +docker logs -f matreon_web_1 +``` + +### Docker Compose + +From the Matreon project directory: + +```sh +export NETWORK=testnet # or "bitcoin" for mainnet +export DATADIR=~/matreon-vol/bitcoin +export LIGHTNING_CHARGE_API_TOKEN=1234 +export FROM_EMAIL="you@example.com" +export BUGS_TO="bugs@example.com" +export SECRET_KEY_BASE=`hexdump -n 64 -e '16/4 "%08x" 1 "\n"' /dev/random` +export DEVISE_SECRET_KEY=`hexdump -n 64 -e '16/4 "%08x" 1 "\n"' /dev/random` +export HOSTNAME=http://localhost +export SMTP_HOST=... +export SMTP_USERNAME=... +export SMTP_PASSWORD=... +docker-compose build +docker-compose up -d +``` + +Migrate the database: + +```sh +docker-compose run web rake db:migrate +``` + +Visit [localhost](http://localhost/). + +To shut everything down + +``` +docker-compose down +``` + +### Cron jobs + +Add the following cron jobs (`crontab -e`): + +```sh +0 * * * * cd /usr/local/src/matreon && /usr/local/bin/docker-compose run web rake invoices:process +0 * * * * cd /usr/local/src/matreon && /usr/local/bin/docker-compose run web rake podcast:fetch +``` ## Deploy to Heroku +### Prerequisites + +You need to run [c-lightning](https://github.com/ElementsProject/lightning) and [Lightning Charge](https://github.com/ElementsProject/lightning-charge) somewhere. + +### Heroku + Create a new Heroku app `app-name` and add the Sendgrid Add-On. Clone this repo and: diff --git a/app/javascript/bundles/Matreon/components/Invoices/Invoices.jsx b/app/javascript/bundles/Matreon/components/Invoices/Invoices.jsx index 81b737d..7a26152 100644 --- a/app/javascript/bundles/Matreon/components/Invoices/Invoices.jsx +++ b/app/javascript/bundles/Matreon/components/Invoices/Invoices.jsx @@ -55,7 +55,7 @@ class Invoice extends React.Component {
  • Paid: {invoice.get('paid_at')}
  • } { !invoice.get('paid_at') && invoice.get('status') != "expired" && -
  • Pay with Lightning
  • +
  • Pay with Lightning
  • } diff --git a/app/lib/lightning_charge_proxy.rb b/app/lib/lightning_charge_proxy.rb new file mode 100644 index 0000000..4a34b26 --- /dev/null +++ b/app/lib/lightning_charge_proxy.rb @@ -0,0 +1,7 @@ +class LightningChargeProxy < Rack::Proxy + def rewrite_env(env) + env['HTTP_HOST'] = 'charge' + env['SERVER_PORT'] = 9112 + env + end +end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 1a1db6d..7c6b14e 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -16,7 +16,7 @@ def as_json(options = nil) def url return nil if !charge_invoice_id - return "#{ ENV["LIGHTNING_CHARGE_URL"] }/checkout/#{ charge_invoice_id}" + return "#{ ENV["LIGHTNING_CHARGE_URL"] || ENV['HOSTNAME'] }/checkout/#{ charge_invoice_id}" end def poll! @@ -54,9 +54,9 @@ def self.email_unpaid_once! def invoice_uri(lightning_charge_id=nil) if lightning_charge_id.nil? - return URI.parse("#{ ENV["LIGHTNING_CHARGE_URL"] }/invoice") + return URI.parse("#{ ENV["LIGHTNING_CHARGE_URL"] || "http://charge:9112" }/invoice") else - return URI.parse("#{ ENV["LIGHTNING_CHARGE_URL"] }/invoice/#{ lightning_charge_id }") + return URI.parse("#{ ENV["LIGHTNING_CHARGE_URL"] || "http://charge:9112" }/invoice/#{ lightning_charge_id }") end end diff --git a/app/models/podcast.rb b/app/models/podcast.rb index 5d2982f..8ec4f1f 100644 --- a/app/models/podcast.rb +++ b/app/models/podcast.rb @@ -9,7 +9,7 @@ class Podcast < ApplicationRecord scope :published, -> { where('pub_date <= ?', Time.now.utc) } def self.fetch! - raise PodcastError.new("Podcast feature disabled") unless ENV['PODCAST'] == "1" + return unless ENV['PODCAST'] == "1" ActiveRecord::Base.transaction do # Track episode guids to delete stale ones: episode_guids = [] diff --git a/config/environments/development.rb b/config/environments/development.rb index 7d44df9..edddd35 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,7 @@ Rails.application.configure do # Verifies that versions and hashed value of the package contents in the project's package.json - config.webpacker.check_yarn_integrity = true + # config.webpacker.check_yarn_integrity = true + config.webpacker.check_yarn_integrity = false # Settings specified here will take precedence over those in config/application.rb. diff --git a/config/environments/production.rb b/config/environments/production.rb index 2cafc28..de8636e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,4 @@ +require 'uri' Rails.application.configure do # Verifies that versions and hashed value of the package contents in the project's package.json config.webpacker.check_yarn_integrity = false @@ -48,8 +49,9 @@ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true - config.ssl_options = {redirect: { host: ENV['HOSTNAME'] }} + # Temp. workaround pending SSL support on EC2: check if we're on Heroku + config.force_ssl = ENV['SENDGRID_USERNAME'].present? + config.ssl_options = {redirect: { host: URI(ENV['HOSTNAME']).host }} # Use the lowest log level to ensure availability of diagnostic information # when problems arise. @@ -69,17 +71,20 @@ # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. config.action_mailer.raise_delivery_errors = true - config.action_mailer.delivery_method = :smtp - config.action_mailer.default_url_options = { host: "https://" + ENV['HOSTNAME'] } - ActionMailer::Base.smtp_settings = { - :address => 'smtp.sendgrid.net', - :port => '587', - :authentication => :plain, - :user_name => ENV['SENDGRID_USERNAME'], - :password => ENV['SENDGRID_PASSWORD'], - :domain => 'heroku.com', - :enable_starttls_auto => true - } + config.action_mailer.default_url_options = { host: URI(ENV['HOSTNAME']).host } + + ActionMailer::Base.smtp_settings = { + :address => ENV['SMTP_HOST'] || 'smtp.sendgrid.net', + :port => ENV['SMTP_PORT'] || '587', + :authentication => :plain, + :user_name => ENV['SMTP_USERNAME'] || ENV['SENDGRID_USERNAME'], + :password => ENV['SMTP_PASSWORD'] || ENV['SENDGRID_PASSWORD'], + :enable_starttls_auto => true + } + + if ENV['SENDGRID_USERNAME'] + ActionMailer::Base.smtp_settings[:domain] = 'heroku.com' + end # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). @@ -109,7 +114,9 @@ :email_prefix => "[Matreon Exception] ", :sender_address => ENV['FROM_EMAIL'], :exception_recipients => [ENV['BUGS_TO']] - } + }, + :error_grouping => true, + :ignore_exceptions => ["Rack::Timeout::RequestTimeoutException"] + ExceptionNotifier.ignored_exceptions ExceptionNotifier::Rake.configure end diff --git a/config/initializers/csp.rb b/config/initializers/csp.rb index 5a04393..1653a73 100644 --- a/config/initializers/csp.rb +++ b/config/initializers/csp.rb @@ -27,18 +27,18 @@ block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. connect_src: connect_src, - font_src: %w('self'), + font_src: %w('self' https://fonts.gstatic.com), form_action: %w('self'), # Allow iframe in dev mode to see email previews (toggle between HTML & text won't work): frame_ancestors: Rails.env.production? ? %w('none') : %w('self'), - img_src: %w('self'), + img_src: %w('self' data:), manifest_src: %w('self'), media_src: %w('self'), object_src: %w('none'), # sandbox: true, # true and [] will set a maximally restrictive setting # plugin_types: %w('none'), - script_src: %w('self'), - style_src: %w('self' 'unsafe-inline'), + script_src: %w('self' 'sha256-IlzWR6dzVCqXEAU14doWEOUXkS5MBqVK4CgYx6TjG4c=' 'sha256-I7xUzuU0UQFJfhTnFbqFJTpRHOUM5d8gyfh6+ikTU7Y='), + style_src: %w('self' 'unsafe-inline' https://fonts.googleapis.com/css), worker_src: %w('self'), # upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ # report_uri: %w(https://report-uri.io/example-csp) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 01d2050..a028643 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -8,7 +8,8 @@ # confirmation, reset password and unlock tokens in the database. # Devise will use the `secret_key_base` as its `secret_key` # by default. You can change it below and use your own secret key. - # config.secret_key = '9bff942b7dbc97df72a14b98a37304dc906dcceb2621fa4a70b061b49c0fd0ccab16f9e98c0f0c58fba0f4cc0990a76d364a989f06ddea3ba958a62afb0915c3' + + config.secret_key = ENV['DEVISE_SECRET_KEY'] if Rails.env.production? # ==> Controller configuration # Configure the parent class to the devise controllers. diff --git a/config/routes.rb b/config/routes.rb index e6ca681..09ae7e4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,10 @@ get 'home', to: 'home#index' root to: 'home#index' + + # Map to localhost:9112 for Lightning Charge, if LIGHTNING_CHARGE_URL isn't set: + mount LightningChargeProxy.new => '/checkout' + mount LightningChargeProxy.new => '/static' # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end diff --git a/db/schema.rb b/db/schema.rb index 31ccd8d..4b91902 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,8 +12,11 @@ ActiveRecord::Schema.define(version: 20180502093551) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + create_table "contributions", force: :cascade do |t| - t.integer "user_id" + t.bigint "user_id" t.bigint "amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -22,7 +25,7 @@ end create_table "invoices", force: :cascade do |t| - t.integer "user_id" + t.bigint "user_id" t.bigint "amount" t.datetime "paid_at" t.string "charge_invoice_id" @@ -68,4 +71,6 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "contributions", "users" + add_foreign_key "invoices", "users" end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d30625d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,87 @@ +version: "3" + +services: + bitcoind: + restart: always + image: bitcoind:0.16.0 + environment: + BITCOIN_EXTRA_ARGS: | + ${NETWORK:-testnet}=1 + rpcport=18332 + prune=2000 + maxuploadtarget=100 + peerbloomfilters=0 + rpcuser=bitcoin + rpcpassword=bitcoin + rpcallowip=0.0.0.0/0 + server=1 + wallet=0 + expose: + - "8333" # Bitcoin Mainnet + - "18333" # Bitcoin Testnet + - "18332" # Bitcoin RPC + ports: + - "${BITCOIN_P2P_PORT:-18333}:18333" + volumes: + - ${DATADIR}/bitcoin:/home/bitcoin/.bitcoin + + lightningd: + restart: always + depends_on: + - bitcoind + image: lightningd:latest + expose: + - "9735" + ports: + - "${LIGHTNING_PORT:-9735}:9735" + links: + - bitcoind + volumes: + - ${DATADIR}/lightning:/root/.lightning + entrypoint: + - /usr/bin/lightningd + - --bitcoin-rpcconnect=bitcoind + - --bitcoin-rpcuser=bitcoin + - --bitcoin-rpcport=18332 + - --bitcoin-rpcpassword=bitcoin + - --log-level=debug + - --network=${NETWORK:-testnet} + - --announce-addr=${IP_ADDRESS} + - --bind-addr=0.0.0.0 + charge: + restart: always + depends_on: + - lightningd + environment: + - API_TOKEN=${LIGHTNING_CHARGE_API_TOKEN} + image: charge:latest + expose: + - "9112" + ports: + - "9112:9112" + volumes: + - ${DATADIR}/charge:/data + - ${DATADIR}/lightning:/etc/lightning/ + + db: + image: postgres + volumes: + - ${DATADIR}/pg:/var/lib/postgresql/data + + web: + build: . + environment: + - FROM_EMAIL=${FROM_EMAIL} + - BUGS_TO=${BUGS_TO} + - SMTP_HOST=${SMTP_HOST} + - SMTP_USERNAME=${SMTP_USERNAME} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - HOSTNAME=${HOSTNAME} + - SECRET_KEY_BASE=${SECRET_KEY_BASE} + - DEVISE_SECRET_KEY=${DEVISE_SECRET_KEY} + - DATABASE_URL=postgres://postgres@db:5432 + - LIGHTNING_CHARGE_API_TOKEN=${LIGHTNING_CHARGE_API_TOKEN} + ports: + - "80:3000" + depends_on: + - db diff --git a/package.json b/package.json index 45021a5..67fc4ae 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,6 @@ "popper.js": "1.12" }, "engines": { - "node": "~8.9.4" + "node": "8.X" } } diff --git a/wait_for_ibd.sh b/wait_for_ibd.sh new file mode 100755 index 0000000..234b951 --- /dev/null +++ b/wait_for_ibd.sh @@ -0,0 +1,7 @@ + #!/bin/bash +while sleep 10 +do + if docker-compose exec -T bitcoind bitcoin-cli -datadir=/home/bitcoin/.bitcoin getblocktemplate; then + break + fi +done