[deploy] Enable admin-configured app_domain (#10206)

* Move app_domain from env var to siteconfig *within app area*

* Fix SiteConfig

* Don't change URL.domain yet

* Not use in fastly cache purge yet

* Session store magoo

* Revert domain issue

* Add SetCookie middleware

* Temporarily remove cookie middleware and release tasks

* Re-enable SetCookieDomain

* Add some more comments, get it ready for testing

* Remove host from url options and temporarily show errors

* Allow forwarded host to be cookie

* Fix typo

* Change invalidAuthenticityToken

* Properly set app domain

* Proper Fastly cache

* Remove config.app_domain

* Remove config.app_domain

* Explicitely set remember_user_token

* More play with cookies

* Fiddle with remember_user_token

* Add remember_user_token changes

* Monkeypatch devise

* Sessions controller

* Include rememberable

* Include rememberable

* Proper cookie monkeypatch

* Proper cookie monkeypatch

* Proper cookie monkeypatch

* Remove extra cookie code

* User sign in tweak

* Close the loop

* Experiment with carrierwave public_url

* Finalize carrierwave monkeypatch

* Remove async controller specs

* Change some tests

* Update domain test

* Remove temporary production.rb change

* modify cookie logic

* Remove unneeded changes

* Remove unneeded changes

* Explicitely pass host

* Fix linting

* Fix social image test

* Add some cookie tests

* Modify article url

* Fix canonical urls

* Fix typo

* Remove sessions controller

* Remove devise monkeypatch

* Add monkeypatch

* Remove elsif

* Update spec/helpers/application_helper_spec.rb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Adjust remember_cookie_sync

* Remember user token experimentation

* Fiddle with devise

* Properly configure devise

* Fix typo

* Fix formatting issue

* Add comment about middleware

* Fix typo

Co-authored-by: Vaidehi Joshi <vaidehi.sj@gmail.com>

* Remove commented-out code

Co-authored-by: Michael Kohl <citizen428@dev.to>
Co-authored-by: Vaidehi Joshi <vaidehi.sj@gmail.com>
This commit is contained in:
Ben Halpern 2020-09-11 09:38:34 -04:00 committed by GitHub
parent df245e7e84
commit 3a469a99e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 111 additions and 58 deletions

View file

@ -107,7 +107,7 @@ module Admin
<<~HEREDOC
*New note from #{params['author_name']}:*
*Report status: #{params['feedback_message_status']}*
Report page: https://#{ApplicationConfig['APP_DOMAIN']}/admin/reports/#{params['noteable_id']}
Report page: https://#{SiteConfig.app_domain}/admin/reports/#{params['noteable_id']}
--------
Message: #{params['content']}
HEREDOC

View file

@ -40,9 +40,8 @@ module Admin
end
def bust_link(link)
if link.starts_with?("https://#{ApplicationConfig['APP_DOMAIN']}")
link.sub!("https://#{ApplicationConfig['APP_DOMAIN']}",
"")
if link.starts_with?(URL.url)
link.sub!(URL.url, "")
end
CacheBuster.bust(link)
CacheBuster.bust("#{link}/")

View file

@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base
skip_before_action :track_ahoy_visit
before_action :verify_private_forem
protect_from_forgery with: :exception, prepend: true
before_action :remember_cookie_sync
include SessionCurrentUser
include ValidRequest
@ -10,6 +11,7 @@ class ApplicationController < ActionController::Base
include CachingHeaders
include ImageUploads
include VerifySetupCompleted
include Devise::Controllers::Rememberable
rescue_from ActionView::MissingTemplate, with: :routing_error
@ -144,6 +146,16 @@ class ApplicationController < ActionController::Base
Stripe.log_level = Stripe::LEVEL_INFO
end
def remember_cookie_sync
# Set remember cookie token in case not properly set.
if user_signed_in? &&
cookies[:remember_user_token].blank?
current_user.remember_me = true
current_user.remember_me!
remember_me(current_user)
end
end
protected
def configure_permitted_parameters

View file

@ -1,5 +1,4 @@
class AsyncInfoController < ApplicationController
include Devise::Controllers::Rememberable
# No pundit policy. All actions are unrestricted.
before_action :set_cache_control_headers, only: %i[shell_version]
@ -13,11 +12,6 @@ class AsyncInfoController < ApplicationController
}
return
end
if remember_user_token.blank?
current_user.remember_me = true
current_user.remember_me!
remember_me(current_user)
end
@user = current_user.decorate
respond_to do |format|
format.json do
@ -87,11 +81,6 @@ class AsyncInfoController < ApplicationController
#{current_user&.checked_code_of_conduct}__
#{current_user&.articles_count}__
#{current_user&.pro?}__
#{current_user&.blocking_others_count}__
#{remember_user_token}"
end
def remember_user_token
cookies[:remember_user_token]
#{current_user&.blocking_others_count}__"
end
end

View file

@ -16,6 +16,6 @@ class BufferedArticlesController < ApplicationController
end
paths = relation.pluck(:path)
paths.map { |path| "https://#{ApplicationConfig['APP_DOMAIN']}#{path}" }
paths.map { |path| URL.url(path) }
end
end

View file

@ -22,7 +22,7 @@ class ArticleDecorator < ApplicationDecorator
end
def url
"https://#{ApplicationConfig['APP_DOMAIN']}#{path}"
URL.url(path)
end
def title_length_classification

View file

@ -37,14 +37,15 @@ module CacheBuster
end
def self.bust_fastly_cache(path)
# @forem/systems Fastly-enabled forems don't need "flexible" domains.
HTTParty.post(
"https://api.fastly.com/purge/https://#{ApplicationConfig['APP_DOMAIN']}#{path}",
"https://api.fastly.com/purge/https://#{URL.domain}#{path}",
headers: {
"Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"]
},
)
HTTParty.post(
"https://api.fastly.com/purge/https://#{ApplicationConfig['APP_DOMAIN']}#{path}?i=i",
"https://api.fastly.com/purge/https://#{URL.domain}#{path}?i=i",
headers: {
"Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"]
},

View file

@ -252,7 +252,7 @@ class MarkdownParser
username = mention.delete("@").downcase
if User.find_by(username: username)
<<~HTML
<a class='comment-mentioned-user' href='#{ApplicationConfig['APP_PROTOCOL']}#{ApplicationConfig['APP_DOMAIN']}/#{username}'>@#{username}</a>
<a class='comment-mentioned-user' href='#{ApplicationConfig['APP_PROTOCOL']}#{SiteConfig.app_domain}/#{username}'>@#{username}</a>
HTML
else
mention

View file

@ -50,7 +50,7 @@ module Redcarpet
end
def app_domain
ApplicationConfig["APP_DOMAIN"]
SiteConfig.app_domain
end
def slugify(string)

View file

@ -7,7 +7,7 @@ module URL
end
def self.domain
ApplicationConfig["APP_DOMAIN"]
Rails.application&.initialized? ? SiteConfig.app_domain : ApplicationConfig["APP_DOMAIN"]
end
def self.url(uri = nil)

View file

@ -0,0 +1,15 @@
# Since we must explicitly set the cookie domain in session_store before SiteConfig is available,
# this ensures we properly set the cookie to SiteConfig.app_domain at runtime.
class SetCookieDomain
def initialize(app)
@app = app
end
def call(env)
if Rails.env.production?
env["rack.session.options"][:domain] = ".#{SiteConfig.app_domain}"
end
@app.call(env)
end
end

View file

@ -196,19 +196,19 @@ class Message < ApplicationRecord
end
def rich_link_article(link)
return unless link["href"].include?("//#{ApplicationConfig['APP_DOMAIN']}/") && link["href"].split("/")[4]
return unless link["href"].include?("//#{SiteConfig.app_domain}/") && link["href"].split("/")[4]
Article.find_by(slug: link["href"].split("/")[4].split("?")[0])
end
def rich_link_tag(link)
return unless link["href"].include?("//#{ApplicationConfig['APP_DOMAIN']}/t/")
return unless link["href"].include?("//#{SiteConfig.app_domain}/t/")
Tag.find_by(name: link["href"].split("/t/")[1].split("/")[0])
end
def rich_user_link(link)
return unless link["href"].include?("//#{ApplicationConfig['APP_DOMAIN']}/")
return unless link["href"].include?("//#{SiteConfig.app_domain}/")
User.find_by(username: link["href"].split("/")[3].split("/")[0])
end

View file

@ -11,7 +11,9 @@ class SiteConfig < RailsSettings::Base
STACK_ICON = File.read(Rails.root.join("app/assets/images/stack.svg")).freeze
LIGHTNING_ICON = File.read(Rails.root.join("app/assets/images/lightning.svg")).freeze
# Core setup
field :waiting_on_first_user, type: :boolean, default: !User.exists?
field :app_domain, type: :string, default: ApplicationConfig["APP_DOMAIN"]
# API Tokens
field :health_check_token, type: :string

View file

@ -47,7 +47,7 @@ module Streams
def twitch_stream_updates_url_for_user(user)
Rails.application.routes.url_helpers.user_twitch_stream_updates_url(user_id: user.id,
host: ApplicationConfig["APP_DOMAIN"])
host: SiteConfig.app_domain)
end
end
end

View file

@ -18,7 +18,7 @@ module Articles
end
return legacy_article_social_image unless use_new_social_url?
article_social_preview_url(article, format: :png)
article_social_preview_url(article, format: :png, host: SiteConfig.app_domain)
end
private

View file

@ -1,6 +1,6 @@
{
"name": "<%= community_qualified_name %>",
"short_name": "<%= ApplicationConfig["APP_DOMAIN"] %>",
"short_name": "<%= SiteConfig.app_domain %>",
"description": "<%= SiteConfig.community_description %>",
"start_url": "/",
"display": "standalone",

View file

@ -40,8 +40,8 @@
<link rel="apple-touch-icon" sizes="167x167" href="<%= optimized_image_url(SiteConfig.logo_png, width: 167, fetch_format: "png") %>">
<link href="<%= optimized_image_url(SiteConfig.logo_png, width: 192, fetch_format: "png") %>" rel="icon" sizes="192x192" />
<link href="<%= optimized_image_url(SiteConfig.logo_png, width: 128, fetch_format: "png") %>" rel="icon" sizes="128x128" />
<meta name="apple-mobile-web-app-title" content="<%= ApplicationConfig["APP_DOMAIN"] %>">
<meta name="application-name" content="<%= ApplicationConfig["APP_DOMAIN"] %>">
<meta name="apple-mobile-web-app-title" content="<%= SiteConfig.app_domain %>">
<meta name="application-name" content="<%= SiteConfig.app_domain %>">
<meta property="fb:pages" content="568966383279687" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="/manifest.json" />

View file

@ -2,7 +2,7 @@
<%= render "users/additional_authentication" %>
<% if ApplicationConfig["APP_DOMAIN"] == "dev.to" %>
<% if SiteConfig.app_domain == "dev.to" %>
<%# @forem/oss Temporary disabling of overly DEV-specialized link. %>
<%# We should create a generalized version of the badge page, including a permanent proxied path for the badge image for hotlinking. %>
<div class="crayons-card mb-6 p-6 grid gap-6">

View file

@ -44,6 +44,11 @@ module PracticalDeveloper
config.autoload_paths += Dir["#{config.root}/lib"]
config.eager_load_paths += Dir["#{config.root}/lib"]
# Middlewares folder is not otherwise autorequired.
Dir["./app/middlewares/*.rb"].sort.each do |file|
require file
end
config.active_job.queue_adapter = :sidekiq
config.middleware.use Rack::Deflater
@ -56,6 +61,8 @@ module PracticalDeveloper
# Therefore we disable "per_form_csrf_tokens" for the time being.
config.action_controller.per_form_csrf_tokens = false
config.middleware.use SetCookieDomain
# NOTE: [Rails 6]
# To improve security, Rails embeds the purpose and expiry metadata inside encrypted or signed cookies value.
config.action_dispatch.use_cookies_with_metadata = false

View file

@ -110,13 +110,12 @@ Rails.application.configure do
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
config.app_domain = ENV["APP_DOMAIN"] || "localhost:3000"
protocol = ENV["APP_PROTOCOL"] || "http://"
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
sendgrid_api_key_present = ENV["SENDGRID_API_KEY"].present?
config.action_mailer.default_url_options = { host: protocol + config.app_domain }
config.action_mailer.default_url_options = { host: protocol + ENV["APP_DOMAIN"] }
ActionMailer::Base.smtp_settings = {
address: "smtp.sendgrid.net",
port: "587",
@ -127,14 +126,13 @@ Rails.application.configure do
enable_starttls_auto: true
}
if ENV["HEROKU_APP_URL"].present? && ENV["HEROKU_APP_URL"] != config.app_domain
if ENV["HEROKU_APP_URL"].present? && ENV["HEROKU_APP_URL"] != ENV["APP_DOMAIN"]
config.middleware.use Rack::HostRedirect,
ENV["HEROKU_APP_URL"] => config.app_domain
ENV["HEROKU_APP_URL"] => ENV["APP_DOMAIN"]
end
end
# rubocop:enable Metrics/BlockLength
Rails.application.routes.default_url_options = {
host: Rails.application.config.app_domain,
protocol: (ENV["APP_PROTOCOL"] || "http://").delete_suffix("://")
}

View file

@ -1,14 +1,16 @@
# @forem/systems Force "public_url" even when fog_public is false via this monkeypatch
# Because we still want the "public" version path in all current scenarios.
# We also force this to use the SiteConfig instead of APP_DOMAIN because the value
# could change after initial boot.
if Rails.env.production? && ENV["HEROKU_APP_ID"].blank?
if Rails.env.production?
module CarrierWave
module Storage
class Fog < Abstract
class File
include CarrierWave::Utilities::Uri
def url
public_url
public_url.gsub(ApplicationConfig["APP_DOMAIN"], SiteConfig.app_domain)
end
end
end

View file

@ -0,0 +1,24 @@
# Changing the value for "domain" in each context instead of using the one set on boot.
# This allows changing domain settings without restarting app.
module Devise
module Controllers
module Rememberable
# We need to use SiteConfig.app_domain instead of default Rails config on boot
def remember_cookie_values(resource)
options = { httponly: true }
options.merge!(forget_cookie_values(resource))
options.merge!(
value: resource.class.serialize_into_cookie(resource),
expires: resource.remember_expires_at,
domain: ".#{SiteConfig.app_domain}",
)
end
def self.cookie_values
# Default: Rails.configuration.session_options.slice(:path, :domain, :secure)
# We need to use SiteConfig.app_domain instead of default Rails config on boot
{ domain: ".#{SiteConfig.app_domain}", secure: ApplicationConfig["FORCE_SSL_IN_RAILS"] == "true" }
end
end
end
end

View file

@ -1,5 +1,7 @@
require "sitemap_generator/s3_adapter"
# @forem/systems: It's fine if this doesn't 100% work correctly right now, as long as it doesn't break stuff at runtime.
if Rails.env.production?
region = ApplicationConfig["AWS_UPLOAD_REGION"].presence || ApplicationConfig["AWS_DEFAULT_REGION"]
config_hash = if ENV["FOREM_CONTEXT"] == "forem_cloud" # @forem/systems jdoss's special sauce.

View file

@ -46,7 +46,7 @@ RSpec.describe ArticleDecorator, type: :decorator do
it "returns the article url without a canonical_url" do
article.canonical_url = ""
expected_url = "https://#{ApplicationConfig['APP_DOMAIN']}#{article.path}"
expected_url = "#{ApplicationConfig['APP_PROTOCOL']}#{ApplicationConfig['APP_DOMAIN']}#{article.path}"
expect(article.decorate.processed_canonical_url).to eq(expected_url)
end
end
@ -77,7 +77,7 @@ RSpec.describe ArticleDecorator, type: :decorator do
describe "#url" do
it "returns the article url" do
expected_url = "https://#{ApplicationConfig['APP_DOMAIN']}#{article.path}"
expected_url = "#{ApplicationConfig['APP_PROTOCOL']}#{ApplicationConfig['APP_DOMAIN']}#{article.path}"
expect(article.decorate.url).to eq(expected_url)
end
end

View file

@ -82,6 +82,7 @@ RSpec.describe ApplicationHelper, type: :helper do
before do
allow(ApplicationConfig).to receive(:[]).with("APP_PROTOCOL").and_return("https://")
allow(ApplicationConfig).to receive(:[]).with("APP_DOMAIN").and_return("dev.to")
allow(SiteConfig).to receive(:app_domain).and_return("dev.to")
end
it "creates the correct base app URL" do

View file

@ -36,9 +36,10 @@ describe SocialImageHelper do
describe ".article_social_image_url" do
it "returns social preview path for newer articles" do
SiteConfig.app_domain = "hello.com"
url = helper.article_social_image_url(article)
expect(url).to eq article_social_preview_url(article, format: :png)
expect(url).to eq article_social_preview_url(article, format: :png, host: "hello.com")
end
it "returns the main image if set" do
@ -59,9 +60,10 @@ describe SocialImageHelper do
end
it "returns social preview path for newer decorated articles" do
SiteConfig.app_domain = "hello.com"
url = helper.article_social_image_url(article.decorate)
expect(url).to eq article_social_preview_url(article, format: :png)
expect(url).to eq article_social_preview_url(article, format: :png, host: "hello.com")
end
it "returns correct manipulation of cloudinary links" do

View file

@ -3,7 +3,8 @@ require "rails_helper"
RSpec.describe URL, type: :lib do
before do
allow(ApplicationConfig).to receive(:[]).with("APP_PROTOCOL").and_return("https://")
allow(ApplicationConfig).to receive(:[]).with("APP_DOMAIN").and_return("dev.to")
allow(ApplicationConfig).to receive(:[]).with("APP_DOMAIN").and_return("test.forem.cloud")
SiteConfig.app_domain = "dev.to"
end
describe ".protocol" do
@ -13,8 +14,8 @@ RSpec.describe URL, type: :lib do
end
describe ".domain" do
it "returns the value of APP_DOMAIN env variable" do
expect(described_class.domain).to eq(ApplicationConfig["APP_DOMAIN"])
it "returns the value of SiteConfig" do
expect(described_class.domain).to eq(SiteConfig.app_domain)
end
end

View file

@ -23,7 +23,6 @@ RSpec.describe "AsyncInfo", type: :request do
context "when logged in" do
it "returns token and user" do
allow(controller_instance).to receive(:remember_user_token).and_return(nil)
sign_in create(:user)
get "/async_info/base_data"
@ -38,15 +37,4 @@ RSpec.describe "AsyncInfo", type: :request do
expect(response.body).to include("version")
end
end
describe "#remember_user_token" do
# We require the remember_user_token key bc we also use it for caching in Fastly
# If this key changes, Fastly needs to be updated
it "requires remember_user_token cookie to be present" do
get "/async_info/base_data"
token = "a_token"
controller.__send__("cookies")[:remember_user_token] = "a_token"
expect(controller.remember_user_token).to eq(token)
end
end
end

View file

@ -385,6 +385,11 @@ RSpec.describe "StoriesIndex", type: :request do
get "/t/#{tag.name}"
expect(response.body).not_to include('<span class="olderposts-pagenumber">')
end
it "sets remember_user_token" do
get "/t/#{tag.name}"
expect(response.cookies["remember_user_token"]).not_to be nil
end
end
context "without user signed in" do
@ -447,6 +452,11 @@ RSpec.describe "StoriesIndex", type: :request do
expected_tag = "<link rel=\"canonical\" href=\"http://localhost:3000/t/#{tag.name}/page/2\" />"
expect(response.body).to include(expected_tag)
end
it "sets does not set remember_user_token" do
get "/t/#{tag.name}"
expect(response.cookies["remember_user_token"]).to be nil
end
end
end