From 5c58cd5e89579df42364129dd9ca4d4644963c1a Mon Sep 17 00:00:00 2001
From: Muhsin
Date: Tue, 17 Feb 2026 16:59:25 +0530
Subject: [PATCH] feat: add SHOPIFY_APP_STORE_URL
---
.../integrations/shopify_controller.rb | 51 ++++++-----
.../shopify/callbacks_controller.rb | 16 +++-
.../super_admin/app_configs_controller.rb | 2 +-
app/javascript/dashboard/api/integrations.js | 6 --
.../settings/integrations/Shopify.vue | 85 +++----------------
app/models/integrations/app.rb | 2 +
config/installation_config.yml | 4 +
config/routes.rb | 1 -
8 files changed, 56 insertions(+), 111 deletions(-)
diff --git a/app/controllers/api/v1/accounts/integrations/shopify_controller.rb b/app/controllers/api/v1/accounts/integrations/shopify_controller.rb
index 5e894a005..147019dc1 100644
--- a/app/controllers/api/v1/accounts/integrations/shopify_controller.rb
+++ b/app/controllers/api/v1/accounts/integrations/shopify_controller.rb
@@ -1,26 +1,9 @@
class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::BaseController
include Shopify::IntegrationHelper
before_action :setup_shopify_context, only: [:orders]
- before_action :fetch_hook, except: [:auth, :complete_install]
+ before_action :fetch_hook, except: [:complete_install]
before_action :validate_contact, only: [:orders]
- def auth
- shop_domain = params[:shop_domain]
- return render json: { error: 'Shop domain is required' }, status: :unprocessable_entity if shop_domain.blank?
-
- state = generate_shopify_token(Current.account.id)
-
- auth_url = "https://#{shop_domain}/admin/oauth/authorize?"
- auth_url += URI.encode_www_form(
- client_id: client_id,
- scope: REQUIRED_SCOPES.join(','),
- redirect_uri: redirect_uri,
- state: state
- )
-
- render json: { redirect_url: auth_url }
- end
-
def orders
customers = fetch_customers
return render json: { orders: [] } if customers.empty?
@@ -32,10 +15,9 @@ class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::Ba
end
def complete_install
- pending_data = ::Redis::Alfred.get("shopify_pending_install:#{params[:pending_install_token]}")
- return render json: { error: 'Invalid or expired install token' }, status: :unprocessable_entity if pending_data.blank?
-
- data = JSON.parse(pending_data)
+ token_key = "shopify_pending_install:#{params[:pending_install_token]}"
+ data = claim_pending_install_token(token_key)
+ return render json: { error: data[:error] }, status: :unprocessable_entity if data[:error]
Current.account.hooks.create!(
app_id: 'shopify',
@@ -45,7 +27,7 @@ class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::Ba
settings: { scope: data['scope'] }
)
- ::Redis::Alfred.delete("shopify_pending_install:#{params[:pending_install_token]}")
+ ::Redis::Alfred.delete(token_key)
head :ok
end
@@ -58,10 +40,6 @@ class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::Ba
private
- def redirect_uri
- "#{ENV.fetch('FRONTEND_URL', '')}/shopify/callback"
- end
-
def contact
@contact ||= Current.account.contacts.find_by(id: params[:contact_id])
end
@@ -126,4 +104,23 @@ class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::Ba
render json: { error: 'Contact information missing' },
status: :unprocessable_entity
end
+
+ def claim_pending_install_token(token_key)
+ pending_data = ::Redis::Alfred.get(token_key)
+ return { error: 'Invalid or expired install token' } if pending_data.blank?
+
+ data = JSON.parse(pending_data)
+
+ if data['claimed']
+ ::Redis::Alfred.delete(token_key)
+ return { error: 'Install token already used' }
+ end
+
+ # Mark as claimed to prevent replay
+ data['claimed'] = true
+ ttl = ::Redis::Alfred.ttl(token_key)
+ ::Redis::Alfred.setex(token_key, data.to_json, [ttl, 60].max) if ttl.positive?
+
+ data
+ end
end
diff --git a/app/controllers/shopify/callbacks_controller.rb b/app/controllers/shopify/callbacks_controller.rb
index 973cedee7..8c8bcb082 100644
--- a/app/controllers/shopify/callbacks_controller.rb
+++ b/app/controllers/shopify/callbacks_controller.rb
@@ -28,10 +28,17 @@ class Shopify::CallbacksController < ApplicationController
end
def handle_shopify_initiated_flow
+ raise StandardError, 'Invalid shop domain' unless valid_shop_domain?
+
@response = oauth_client.auth_code.get_token(params[:code], redirect_uri: redirect_callback_uri)
token_key = SecureRandom.hex(16)
- pending_data = { access_token: parsed_body['access_token'], shop: params[:shop], scope: parsed_body['scope'] }.to_json
+ pending_data = {
+ access_token: parsed_body['access_token'],
+ shop: params[:shop],
+ scope: parsed_body['scope'],
+ claimed: false
+ }.to_json
::Redis::Alfred.setex("shopify_pending_install:#{token_key}", pending_data, 10.minutes)
redirect_url = "settings/integrations/shopify?shopify_pending_install=#{token_key}"
@@ -91,4 +98,11 @@ class Shopify::CallbacksController < ApplicationController
def frontend_url
ENV.fetch('FRONTEND_URL', '')
end
+
+ def valid_shop_domain?
+ return false if params[:shop].blank?
+
+ # Shopify shop domains must match: *.myshopify.com or *.myshopify.io (for dev shops)
+ params[:shop].match?(/\A[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.(com|io)\z/)
+ end
end
diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb
index 67d58aef1..746dd1af3 100644
--- a/app/controllers/super_admin/app_configs_controller.rb
+++ b/app/controllers/super_admin/app_configs_controller.rb
@@ -40,7 +40,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
def allowed_configs
mapping = {
'facebook' => %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT],
- 'shopify' => %w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET],
+ 'shopify' => %w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET SHOPIFY_APP_STORE_URL],
'microsoft' => %w[AZURE_APP_ID AZURE_APP_SECRET],
'email' => %w[MAILER_INBOUND_EMAIL_DOMAIN ACCOUNT_EMAILS_LIMIT ACCOUNT_EMAILS_PLAN_LIMITS],
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET],
diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js
index d4ffcbca3..2b816e603 100644
--- a/app/javascript/dashboard/api/integrations.js
+++ b/app/javascript/dashboard/api/integrations.js
@@ -32,12 +32,6 @@ class IntegrationsAPI extends ApiClient {
deleteHook(hookId) {
return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`);
}
-
- connectShopify({ shopDomain }) {
- return axios.post(`${this.baseUrl()}/integrations/shopify/auth`, {
- shop_domain: shopDomain,
- });
- }
}
export default new IntegrationsAPI();
diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Shopify.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Shopify.vue
index 6d09d2c40..dba5f36fd 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Shopify.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Shopify.vue
@@ -11,11 +11,8 @@ import { useAlert } from 'dashboard/composables';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import Integration from './Integration.vue';
import Spinner from 'shared/components/Spinner.vue';
-import integrationAPI from 'dashboard/api/integrations';
import shopifyAPI from 'dashboard/api/integrations/shopify';
-import Input from 'dashboard/components-next/input/Input.vue';
-import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
@@ -30,11 +27,7 @@ const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { formatMessage } = useMessageFormatter();
-const dialogRef = ref(null);
const integrationLoaded = ref(false);
-const storeUrl = ref('');
-const isSubmitting = ref(false);
-const storeUrlError = ref('');
const integration = useFunctionGetter('integrations/getIntegration', 'shopify');
const uiFlags = useMapGetter('integrations/getUIFlags');
@@ -62,48 +55,6 @@ const formattedHelpText = computed(() => {
);
});
-const hideStoreUrlModal = () => {
- storeUrl.value = '';
- storeUrlError.value = '';
- isSubmitting.value = false;
-};
-
-const validateStoreUrl = url => {
- const pattern = /^[a-zA-Z0-9][a-zA-Z0-9-]*\.myshopify\.com$/;
- return pattern.test(url);
-};
-
-const openStoreUrlDialog = () => {
- if (dialogRef.value) {
- dialogRef.value.open();
- }
-};
-
-const handleStoreUrlSubmit = async () => {
- try {
- storeUrlError.value = '';
- if (!validateStoreUrl(storeUrl.value)) {
- storeUrlError.value = t(
- 'INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.INVALID_URL'
- );
- return;
- }
-
- isSubmitting.value = true;
- const { data } = await integrationAPI.connectShopify({
- shopDomain: storeUrl.value,
- });
-
- if (data.redirect_url) {
- window.location.href = data.redirect_url;
- }
- } catch (error) {
- storeUrlError.value = error.message;
- } finally {
- isSubmitting.value = false;
- }
-};
-
const completePendingInstall = async token => {
try {
await shopifyAPI.completeInstall(token);
@@ -150,11 +101,16 @@ onMounted(() => {
}"
>
-
+
+
+
@@ -178,27 +134,6 @@ onMounted(() => {
{{ $t('INTEGRATION_SETTINGS.SHOPIFY.ERROR') }}
-
diff --git a/app/models/integrations/app.rb b/app/models/integrations/app.rb
index 5e4d28c06..0ef1f54fc 100644
--- a/app/models/integrations/app.rb
+++ b/app/models/integrations/app.rb
@@ -43,6 +43,8 @@ class Integrations::App
"#{params[:action]}&client_id=#{client_id}&redirect_uri=#{self.class.slack_integration_url}"
when 'linear'
build_linear_action
+ when 'shopify'
+ GlobalConfigService.load('SHOPIFY_APP_STORE_URL', nil)
else
params[:action]
end
diff --git a/config/installation_config.yml b/config/installation_config.yml
index 34cb736bf..3b0bfdcec 100644
--- a/config/installation_config.yml
+++ b/config/installation_config.yml
@@ -387,6 +387,10 @@
description: 'The Client Secret (API Secret Key) from your Shopify Partner account'
locked: false
type: secret
+- name: SHOPIFY_APP_STORE_URL
+ display_title: 'Shopify App Store URL'
+ description: 'The Shopify App Store listing URL (e.g., https://apps.shopify.com/your-app)'
+ locked: false
# ------- End of Shopify Related Config ------- #
# ------- Instagram Channel Related Config ------- #
diff --git a/config/routes.rb b/config/routes.rb
index 0a99f525f..03120a652 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -315,7 +315,6 @@ Rails.application.routes.draw do
end
resource :shopify, controller: 'shopify', only: [:destroy] do
collection do
- post :auth
get :orders
post :complete_install
end