From cf73ba195e0f62844ea3e5af9b2dd05f79f32531 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 12 Jun 2025 11:55:57 +0530 Subject: [PATCH] feat: add github auth --- .../integrations/github_controller.rb | 13 ++++ .../github/callbacks_controller.rb | 70 +++++++++++++++++ .../super_admin/app_configs_controller.rb | 3 +- app/helpers/github/integration_helper.rb | 71 ++++++++++++++++++ .../i18n/locale/en/integrations.json | 22 ++++++ .../settings/integrations/Github.vue | 57 ++++++++++++++ .../integrations/integrations.routes.js | 9 +++ app/models/integrations/app.rb | 18 +++++ .../super_admin/application/_icons.html.erb | 3 + config/features.yml | 3 + config/installation_config.yml | 14 ++++ config/integration/apps.yml | 7 ++ config/locales/en.yml | 4 + config/routes.rb | 4 + .../app/helpers/super_admin/features.yml | 6 ++ .../images/integrations/github-dark.png | Bin 0 -> 8776 bytes .../dashboard/images/integrations/github.png | Bin 0 -> 9316 bytes 17 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v1/accounts/integrations/github_controller.rb create mode 100644 app/controllers/github/callbacks_controller.rb create mode 100644 app/helpers/github/integration_helper.rb create mode 100644 app/javascript/dashboard/routes/dashboard/settings/integrations/Github.vue create mode 100644 public/dashboard/images/integrations/github-dark.png create mode 100644 public/dashboard/images/integrations/github.png diff --git a/app/controllers/api/v1/accounts/integrations/github_controller.rb b/app/controllers/api/v1/accounts/integrations/github_controller.rb new file mode 100644 index 000000000..33b3bd53f --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/github_controller.rb @@ -0,0 +1,13 @@ +class Api::V1::Accounts::Integrations::GithubController < Api::V1::Accounts::BaseController + before_action :check_authorization + + def show + @github_integration = Current.account.hooks.find_by(app_id: 'github') + end + + private + + def check_authorization + authorize ::Webhook, :show? + end +end \ No newline at end of file diff --git a/app/controllers/github/callbacks_controller.rb b/app/controllers/github/callbacks_controller.rb new file mode 100644 index 000000000..ae71aafea --- /dev/null +++ b/app/controllers/github/callbacks_controller.rb @@ -0,0 +1,70 @@ +class Github::CallbacksController < ApplicationController + include Github::IntegrationHelper + before_action :authenticate_user! + before_action :set_account + + def show + if params[:error].present? + redirect_to app_account_integrations_path(account_id: @account.id), alert: params[:error_description] + return + end + + if params[:code].present? + response = exchange_code_for_token(params[:code]) + + if response[:access_token].present? + hook = @account.hooks.find_or_initialize_by(app_id: 'github') + hook.access_token = response[:access_token] + hook.status = 'enabled' + + if hook.save + redirect_to app_account_integrations_path(account_id: @account.id), notice: 'GitHub integration connected successfully!' + else + redirect_to app_account_integrations_path(account_id: @account.id), alert: 'Failed to save GitHub integration' + end + else + redirect_to app_account_integrations_path(account_id: @account.id), alert: 'Failed to get access token from GitHub' + end + else + redirect_to app_account_integrations_path(account_id: @account.id), alert: 'GitHub authorization failed' + end + end + + private + + def set_account + @account = Current.user.accounts.find(params[:account_id]) if params[:account_id].present? + @account ||= Current.user.accounts.first + + return if @account + + redirect_to root_path, alert: 'Account not found' + end + + def exchange_code_for_token(code) + client_id = GlobalConfigService.load('GITHUB_CLIENT_ID', nil) + client_secret = GlobalConfigService.load('GITHUB_CLIENT_SECRET', nil) + + return {} unless client_id && client_secret + + response = HTTParty.post('https://github.com/login/oauth/access_token', { + body: { + client_id: client_id, + client_secret: client_secret, + code: code + }, + headers: { + 'Accept' => 'application/json' + } + }) + + if response.success? + JSON.parse(response.body).with_indifferent_access + else + {} + end + rescue StandardError => e + Rails.logger.error "GitHub OAuth error: #{e.message}" + {} + end +end \ No newline at end of file diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 204bfc95b..668114d35 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -39,7 +39,8 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController 'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'], 'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET], 'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET], - 'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT] + 'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT], + 'github' => %w[GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET] } @allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]) diff --git a/app/helpers/github/integration_helper.rb b/app/helpers/github/integration_helper.rb new file mode 100644 index 000000000..fec046f38 --- /dev/null +++ b/app/helpers/github/integration_helper.rb @@ -0,0 +1,71 @@ +module Github::IntegrationHelper + def github_integration_url(account_id) + "#{ENV.fetch('FRONTEND_URL', nil)}/github/callback?account_id=#{account_id}" + end + + def github_oauth_url(account_id) + client_id = GlobalConfigService.load('GITHUB_CLIENT_ID', nil) + return nil unless client_id + + params = { + client_id: client_id, + redirect_uri: github_integration_url(account_id), + scope: 'repo', + state: generate_state_token(account_id) + } + + "https://github.com/login/oauth/authorize?#{params.to_query}" + end + + def github_configured? + GlobalConfigService.load('GITHUB_CLIENT_ID', nil).present? && + GlobalConfigService.load('GITHUB_CLIENT_SECRET', nil).present? + end + + def github_integration_enabled?(account) + return false unless github_configured? + + account.hooks.exists?(app_id: 'github', status: 'enabled') + end + + def github_repositories(access_token) + return [] unless access_token + + response = HTTParty.get('https://api.github.com/user/repos', { + headers: { + 'Authorization' => "token #{access_token}", + 'Accept' => 'application/vnd.github.v3+json' + }, + query: { + per_page: 100, + sort: 'updated' + } + }) + + if response.success? + JSON.parse(response.body) + else + [] + end + rescue StandardError => e + Rails.logger.error "GitHub API error: #{e.message}" + [] + end + + private + + def generate_state_token(account_id) + payload = { + account_id: account_id, + timestamp: Time.current.to_i + } + + JWT.encode(payload, Rails.application.secret_key_base, 'HS256') + end + + def verify_state_token(state) + JWT.decode(state, Rails.application.secret_key_base, true, { algorithm: 'HS256' }) + rescue JWT::DecodeError + nil + end +end \ No newline at end of file diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index 071c95604..0d3bccbb6 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -328,6 +328,28 @@ "DESCRIPTION": "Linear workspace is not connected. Click the button below to connect your workspace to use this integration.", "BUTTON_TEXT": "Connect Linear workspace" } + }, + "GITHUB": { + "TITLE": "GitHub Integration", + "DESCRIPTION": "Connect your GitHub repositories to create issues directly from customer conversations. This integration helps you track bugs, feature requests, and development tasks seamlessly.", + "NOT_CONFIGURED": { + "TITLE": "GitHub Integration Not Configured", + "DESCRIPTION": "Contact your administrator to configure GitHub OAuth settings." + }, + "CONNECT": { + "TITLE": "Connect GitHub", + "DESCRIPTION": "Connect your GitHub account to start creating issues from conversations.", + "BUTTON": "Connect GitHub" + }, + "CONNECTED": { + "TITLE": "GitHub Connected", + "DESCRIPTION": "Your GitHub account is successfully connected. You can now create issues from conversations." + }, + "DISCONNECT": { + "BUTTON": "Disconnect", + "SUCCESS": "GitHub integration disconnected successfully", + "ERROR": "Failed to disconnect GitHub integration" + } } }, "CAPTAIN": { diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Github.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Github.vue new file mode 100644 index 000000000..78e6b7314 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Github.vue @@ -0,0 +1,57 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js b/app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js index e50eccb3a..846c220ff 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/integrations.routes.js @@ -9,6 +9,7 @@ import Slack from './Slack.vue'; import SettingsContent from '../Wrapper.vue'; import Linear from './Linear.vue'; import Shopify from './Shopify.vue'; +import Github from './Github.vue'; export default { routes: [ @@ -100,6 +101,14 @@ export default { }, props: route => ({ error: route.query.error }), }, + { + path: 'github', + name: 'settings_integrations_github', + component: Github, + meta: { + permissions: ['administrator'], + }, + }, { path: ':integration_id', name: 'settings_applications_integration', diff --git a/app/models/integrations/app.rb b/app/models/integrations/app.rb index dfe889bfa..595777cce 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 'github' + build_github_action else params[:action] end @@ -58,6 +60,8 @@ class Integrations::App account.feature_enabled?('shopify_integration') && GlobalConfigService.load('SHOPIFY_CLIENT_ID', nil).present? when 'leadsquared' account.feature_enabled?('crm_integration') + when 'github' + GlobalConfigService.load('GITHUB_CLIENT_ID', nil).present? else true end @@ -75,6 +79,16 @@ class Integrations::App ].join('&') end + def build_github_action + client_id = GlobalConfigService.load('GITHUB_CLIENT_ID', nil) + [ + 'https://github.com/login/oauth/authorize?response_type=code', + "client_id=#{client_id}", + "redirect_uri=#{self.class.github_integration_url}", + 'scope=repo' + ].join('&') + end + def enabled?(account) case params[:id] when 'webhook' @@ -98,6 +112,10 @@ class Integrations::App "#{ENV.fetch('FRONTEND_URL', nil)}/linear/callback" end + def self.github_integration_url + "#{ENV.fetch('FRONTEND_URL', nil)}/github/callback" + end + class << self def apps Hashie::Mash.new(APPS_CONFIG) diff --git a/app/views/super_admin/application/_icons.html.erb b/app/views/super_admin/application/_icons.html.erb index fabff914b..1aaff5dbb 100644 --- a/app/views/super_admin/application/_icons.html.erb +++ b/app/views/super_admin/application/_icons.html.erb @@ -159,6 +159,9 @@ + + + diff --git a/config/features.yml b/config/features.yml index 131456c72..fb40b1936 100644 --- a/config/features.yml +++ b/config/features.yml @@ -168,4 +168,7 @@ enabled: true - name: crm_integration display_name: CRM Integration + enabled: false +- name: github_integration + display_name: Github Integration enabled: false \ No newline at end of file diff --git a/config/installation_config.yml b/config/installation_config.yml index 66538d4ab..dea95d63d 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -341,3 +341,17 @@ value: 'v22.0' locked: true # ------- End of Instagram Channel Related Config ------- # + +## ------ Configs added for GitHub ------ ## +- name: GITHUB_CLIENT_ID + display_title: 'GitHub Client ID' + value: + locked: false + description: 'GitHub OAuth app client ID' +- name: GITHUB_CLIENT_SECRET + display_title: 'GitHub Client Secret' + value: + locked: false + description: 'GitHub OAuth app client secret' + type: secret +## ------ End of Configs added for GitHub ------ ## diff --git a/config/integration/apps.yml b/config/integration/apps.yml index 1faf35670..928038bdb 100644 --- a/config/integration/apps.yml +++ b/config/integration/apps.yml @@ -278,3 +278,10 @@ leadsquared: 'conversation_activity_score', 'transcript_activity_score', ] + +github: + id: github + logo: github.png + i18n_key: github + hook_type: account + allow_multiple_hooks: false diff --git a/config/locales/en.yml b/config/locales/en.yml index 463aa713e..78b5a9770 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -257,6 +257,10 @@ en: name: 'LeadSquared' short_description: 'Sync your contacts and conversations with LeadSquared CRM.' description: 'Sync your contacts and conversations with LeadSquared CRM. This integration automatically creates leads in LeadSquared when new contacts are added, and logs conversation activity to provide your sales team with complete context.' + github: + name: 'GitHub' + short_description: 'Create and manage GitHub issues directly from conversations.' + description: 'Connect your GitHub repositories to create issues directly from customer conversations. This integration helps you track bugs, feature requests, and development tasks seamlessly.' captain: copilot_error: 'Please connect an assistant to this inbox to use Copilot' copilot_limit: 'You are out of Copilot credits. You can buy more credits from the billing section.' diff --git a/config/routes.rb b/config/routes.rb index 1ed080457..be884150b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -265,6 +265,9 @@ Rails.application.routes.draw do get :linked_issues end end + namespace :integrations do + resource :github, only: [:show] + end end resources :working_hours, only: [:update] @@ -493,6 +496,7 @@ Rails.application.routes.draw do get 'microsoft/callback', to: 'microsoft/callbacks#show' get 'google/callback', to: 'google/callbacks#show' get 'instagram/callback', to: 'instagram/callbacks#show' + get 'github/callback', to: 'github/callbacks#show' # ---------------------------------------------------------------------- # Routes for external service verifications get '.well-known/assetlinks.json' => 'android_app#assetlinks' diff --git a/enterprise/app/helpers/super_admin/features.yml b/enterprise/app/helpers/super_admin/features.yml index c20de2dfa..de83ef61e 100644 --- a/enterprise/app/helpers/super_admin/features.yml +++ b/enterprise/app/helpers/super_admin/features.yml @@ -103,3 +103,9 @@ shopify: enabled: true icon: 'icon-shopify' config_key: 'shopify' +github: + name: 'GitHub' + description: 'Configuration for setting up GitHub Integration' + enabled: true + icon: 'icon-github' + config_key: 'github' diff --git a/public/dashboard/images/integrations/github-dark.png b/public/dashboard/images/integrations/github-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d66bdeb478a15c129047eab270e54a9f19049800 GIT binary patch literal 8776 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 literal 0 HcmV?d00001 diff --git a/public/dashboard/images/integrations/github.png b/public/dashboard/images/integrations/github.png new file mode 100644 index 0000000000000000000000000000000000000000..10b895fc85584b3bde007b2a3dd25ddd57c6c759 GIT binary patch literal 9316 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 MP}5N@QAUUSALzZ|wg3PC literal 0 HcmV?d00001