chore: Automate SSL with Cloudflare (#12021)
This PR adds support for automatic SSL issuance using Cloudflare when a custom domain is updated. - Introduced a cloudflare configuration. If present, the system will attempt to issue an SSL certificate via Cloudflare whenever a custom domain is added or changed. - SSL verification is handled using an HTTP challenge. - The job will store the HTTP challenge response provided by Cloudflare and serve it under the /.well-known/cf path automatically. How to test: - Create a Cloudflare zone for your domain and copy the Zone ID. - Generate a Cloudflare API token with the required SSL certificate permissions. - Set the Fallback Origin under SSL -> Custom HostName to the Chatwoot installation. - Add or update a custom domain and verify that the SSL certificate is automatically issued. --------- Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
class Cloudflare::BaseCloudflareZoneService
|
||||
BASE_URI = 'https://api.cloudflare.com/client/v4'.freeze
|
||||
|
||||
private
|
||||
|
||||
def headers
|
||||
{
|
||||
'Authorization' => "Bearer #{api_token}",
|
||||
'Content-Type' => 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
def api_token
|
||||
InstallationConfig.find_by(name: 'CLOUDFLARE_API_KEY')&.value
|
||||
end
|
||||
|
||||
def zone_id
|
||||
InstallationConfig.find_by(name: 'CLOUDFLARE_ZONE_ID')&.value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
class Cloudflare::CheckCustomHostnameService < Cloudflare::BaseCloudflareZoneService
|
||||
pattr_initialize [:portal!]
|
||||
|
||||
def perform
|
||||
return { errors: ['Cloudflare API token or zone ID not found'] } if api_token.blank? || zone_id.blank?
|
||||
return { errors: ['No custom domain found'] } if @portal.custom_domain.blank?
|
||||
|
||||
response = HTTParty.get(
|
||||
"#{BASE_URI}/zones/#{zone_id}/custom_hostnames?hostname=#{@portal.custom_domain}", headers: headers
|
||||
)
|
||||
|
||||
return { errors: response.parsed_response['errors'] } unless response.success?
|
||||
|
||||
data = response.parsed_response['result']
|
||||
|
||||
if data.present?
|
||||
update_portal_ssl_settings(data.first)
|
||||
return { data: data }
|
||||
end
|
||||
|
||||
{ errors: ['Hostname is missing in Cloudflare'] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_portal_ssl_settings(data)
|
||||
verification_record = data['ownership_verification_http']
|
||||
ssl_settings = {
|
||||
'cf_verification_id': verification_record['http_url'].split('/').last,
|
||||
'cf_verification_body': verification_record['http_body']
|
||||
}
|
||||
@portal.update(ssl_settings: ssl_settings)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
class Cloudflare::CreateCustomHostnameService < Cloudflare::BaseCloudflareZoneService
|
||||
pattr_initialize [:portal!]
|
||||
|
||||
def perform
|
||||
return { errors: ['Cloudflare API token or zone ID not found'] } if api_token.blank? || zone_id.blank?
|
||||
return { errors: ['No hostname found'] } if @portal.custom_domain.blank?
|
||||
|
||||
response = create_hostname
|
||||
|
||||
return { errors: response.parsed_response['errors'] } unless response.success?
|
||||
|
||||
data = response.parsed_response['result']
|
||||
|
||||
if data.present?
|
||||
update_portal_ssl_settings(data)
|
||||
return { data: data }
|
||||
end
|
||||
|
||||
{ errors: ['Could not create hostname'] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_hostname
|
||||
HTTParty.post(
|
||||
"#{BASE_URI}/zones/#{zone_id}/custom_hostnames",
|
||||
headers: headers,
|
||||
body: { hostname: @portal.custom_domain }.to_json
|
||||
)
|
||||
end
|
||||
|
||||
def update_portal_ssl_settings(data)
|
||||
verification_record = data['ownership_verification_http']
|
||||
ssl_settings = {
|
||||
'cf_verification_id': verification_record['http_url'].split('/').last,
|
||||
'cf_verification_body': verification_record['http_body']
|
||||
}
|
||||
@portal.update(ssl_settings: ssl_settings)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user