diff --git a/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb b/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb new file mode 100644 index 000000000..936b081f8 --- /dev/null +++ b/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb @@ -0,0 +1,16 @@ +module SpreeCmCommissioner + module ContentCachable + extend ActiveSupport::Concern + + included do + after_action :set_cache_control_for_cdn + end + + def set_cache_control_for_cdn + return unless request.get? || request.head? + return unless request.base_url == ENV['CONTENT_HOST_URL'] + + response.set_header('Cache-Control', 'public, max-age=7200') + end + end +end diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index d36828219..7e12ca77a 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -31,6 +31,29 @@ def parse_date(date, format: nil) nil end + # POST + def invalidate_api_caches + if params[:model].present? + api_patterns_map = { + 'SpreeCmCommissioner::HomepageSection' => '/api/v2/storefront/homepage/*', + 'SpreeCmCommissioner::HomepageBackground' => '/api/v2/storefront/homepage/*', + 'Spree::Menu' => '/api/v2/storefront/menus*' + } + + api_patterns = api_patterns_map[params[:model]] + + if api_patterns.is_a?(Array) + api_patterns.each { |pattern| SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(pattern) } + elsif api_patterns.is_a?(String) + SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(api_patterns) + end + elsif params[:api_pattern].present? + SpreeCmCommissioner::InvalidateCacheRequestJob.perform_later(params[:api_pattern]) + end + + redirect_back fallback_location: admin_root_path + end + private def redirect_to_billing diff --git a/app/controllers/spree_cm_commissioner/action_controller/api_decorator.rb b/app/controllers/spree_cm_commissioner/action_controller/api_decorator.rb index 535f40027..efdc29f09 100644 --- a/app/controllers/spree_cm_commissioner/action_controller/api_decorator.rb +++ b/app/controllers/spree_cm_commissioner/action_controller/api_decorator.rb @@ -3,6 +3,7 @@ module ActionController module ApiDecorator def self.prepended(base) base.include SpreeCmCommissioner::ExceptionNotificable + base.include SpreeCmCommissioner::ContentCachable end # Annonymous block: https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Naming/BlockForwarding diff --git a/app/controllers/spree_cm_commissioner/application_controller_decorator.rb b/app/controllers/spree_cm_commissioner/application_controller_decorator.rb index 430a85f1c..be4740fa1 100644 --- a/app/controllers/spree_cm_commissioner/application_controller_decorator.rb +++ b/app/controllers/spree_cm_commissioner/application_controller_decorator.rb @@ -2,6 +2,7 @@ module SpreeCmCommissioner module ApplicationControllerDecorator def self.prepended(base) base.include SpreeCmCommissioner::ExceptionNotificable + base.include SpreeCmCommissioner::ContentCachable end # Annonymous block: https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Naming/BlockForwarding diff --git a/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb b/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb new file mode 100644 index 000000000..84a1a0ddb --- /dev/null +++ b/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb @@ -0,0 +1,31 @@ +require 'aws-sdk-cloudfront' + +# pattern: '/api/v2/storefront/homepage_sections*' +# https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/invalidation-access-logs.html + +module SpreeCmCommissioner + class InvalidateCacheRequest < BaseInteractor + delegate :pattern, to: :context + + def call + client = ::Aws::CloudFront::Client.new( + region: ENV.fetch('AWS_REGION'), + access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID'), + secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY'), + http_open_timeout: 15, + http_read_timeout: 60 + ) + + context.response = client.create_invalidation( + distribution_id: ENV.fetch('ASSETS_SYNC_CF_DIST_ID'), + invalidation_batch: { + caller_reference: Time.now.to_i.to_s, + paths: { + quantity: 1, + items: [pattern] + } + } + ) + end + end +end diff --git a/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb b/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb new file mode 100644 index 000000000..e1c7ac26d --- /dev/null +++ b/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb @@ -0,0 +1,7 @@ +module SpreeCmCommissioner + class InvalidateCacheRequestJob < ApplicationJob + def perform(pattern) + SpreeCmCommissioner::InvalidateCacheRequest.call(pattern: pattern) + end + end +end diff --git a/app/views/spree/admin/homepage_section/index.html.erb b/app/views/spree/admin/homepage_section/index.html.erb index bfa2da64d..2f7e9d32f 100644 --- a/app/views/spree/admin/homepage_section/index.html.erb +++ b/app/views/spree/admin/homepage_section/index.html.erb @@ -3,6 +3,7 @@ <% end %> <% content_for :page_actions do %> + <%= button_link_to Spree.t(:clear_cache), admin_invalidate_api_caches_path(model: SpreeCmCommissioner::HomepageSection.name), method: :post, class: "btn btn-outline-primary" %> <%= button_link_to Spree.t(:new_homepage_section), new_admin_homepage_feed_homepage_section_path, { class: "btn-success", icon: 'add.svg', id: 'admin_new_homepage_section' } %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index f5f0ae94a..4a44c662b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,8 @@ Spree::Core::Engine.add_routes do # Add your extension routes here namespace :admin do + post '/invalidate_api_caches', to: 'base#invalidate_api_caches' + resources :promotions do resources :custom_dates_rules, controller: :promotion_custom_dates_rules, only: %i[edit update] do member do diff --git a/spec/cassettes/SpreeCmCommissioner_InvalidateCacheRequest/_call/requested_to_invalidate_cache_return_response.yml b/spec/cassettes/SpreeCmCommissioner_InvalidateCacheRequest/_call/requested_to_invalidate_cache_return_response.yml new file mode 100644 index 000000000..8f4fba04d --- /dev/null +++ b/spec/cassettes/SpreeCmCommissioner_InvalidateCacheRequest/_call/requested_to_invalidate_cache_return_response.yml @@ -0,0 +1,51 @@ +--- +http_interactions: +- request: + method: post + uri: https://cloudfront.amazonaws.com/2020-05-31/distribution/D12FAKEFAKE/invalidation + body: + encoding: UTF-8 + string: 1/staging/422.html1729834869 + headers: + Accept-Encoding: + - '' + Content-Type: + - application/xml + User-Agent: + - aws-sdk-ruby3/3.209.1 ua/2.1 api/cloudfront#1.82.0 os/macos#23 md/arm64 lang/ruby#3.2.0 + md/3.2.0 m/D + Host: + - cloudfront.amazonaws.com + X-Amz-Date: + - 20241025T054109Z + X-Amz-Content-Sha256: + - 4231c5527e8a773761d8806a1578ab2d799479bf369bd60e0f5a4af0f08afa9b + Authorization: + - AWS4-HMAC-SHA256 Credential=AXXXXXXXXXXXXXXXY/20241025/us-east-1/cloudfront/aws4_request, + SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=66b25acc5a915f366e1da91e6ab214dfe1eb55c183fb2196836d6df212c696eb + Content-Length: + - '222' + Accept: + - "*/*" + response: + status: + code: 201 + message: Created + headers: + X-Amzn-Requestid: + - 4dc45b65-cabf-4d0a-8253-7955e851cacd + Location: + - https://cloudfront.amazonaws.com/2020-05-31/distribution/D12FAKEFAKE/invalidation/I2PGO1F2LAWEKXXYAKMN0P38N1 + Content-Type: + - text/xml + Content-Length: + - '384' + Date: + - Fri, 25 Oct 2024 05:41:10 GMT + body: + encoding: UTF-8 + string: |- + + I2PGO1F2LAWEKXXYAKMN0P38N1InProgress2024-10-25T05:41:11.383Z1/staging/422.html1729834869 + recorded_at: Fri, 25 Oct 2024 05:41:11 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/interactors/spree_cm_commissioner/invalidate_cache_request_spec.rb b/spec/interactors/spree_cm_commissioner/invalidate_cache_request_spec.rb new file mode 100644 index 000000000..861cc2670 --- /dev/null +++ b/spec/interactors/spree_cm_commissioner/invalidate_cache_request_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +RSpec.describe SpreeCmCommissioner::InvalidateCacheRequest do + before do + ENV['AWS_REGION'] = 'ap-southeast-1' + ENV['AWS_ACCESS_KEY_ID'] = 'AXXXXXXXXXXXXXXXY' + ENV['AWS_SECRET_ACCESS_KEY'] = 'VO103FAKEFAKEFAKE3020X=I230x1' + ENV['ASSETS_SYNC_CF_DIST_ID'] = 'D12FAKEFAKE' + end + + describe '.call', :vcr do + it 'requested to invalidate cache & return response' do + context = described_class.call(pattern: '/staging/422.html') + + expect(context.response.location).to eq "https://cloudfront.amazonaws.com/2020-05-31/distribution/D12FAKEFAKE/invalidation/I2PGO1F2LAWEKXXYAKMN0P38N1" + expect(context.response.invalidation.id).to eq "I2PGO1F2LAWEKXXYAKMN0P38N1" + expect(context.response.invalidation.status).to eq "InProgress" + end + end +end diff --git a/spec/jobs/spree_cm_commissioner/invalidate_cache_request_job_spec.rb b/spec/jobs/spree_cm_commissioner/invalidate_cache_request_job_spec.rb new file mode 100644 index 000000000..f01a7b128 --- /dev/null +++ b/spec/jobs/spree_cm_commissioner/invalidate_cache_request_job_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +RSpec.describe SpreeCmCommissioner::InvalidateCacheRequestJob do + describe '#perform' do + it 'invokes StateUpdater.call' do + expect(SpreeCmCommissioner::InvalidateCacheRequest).to receive(:call).with(pattern: '/api/storefront/*') + described_class.new.perform('/api/storefront/*') + end + end +end