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 000000000..d66bdeb47 Binary files /dev/null and b/public/dashboard/images/integrations/github-dark.png differ diff --git a/public/dashboard/images/integrations/github.png b/public/dashboard/images/integrations/github.png new file mode 100644 index 000000000..10b895fc8 Binary files /dev/null and b/public/dashboard/images/integrations/github.png differ