| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | # Connects a user's GitHub account for repository imports via our own GitHub |
| 4 | # OAuth app (separate from smbCloud sign-in). The resulting token is stored |
| 5 | # encrypted in GithubConnection and never leaves the server. |
| 6 | class GithubConnectionsController < ApplicationController |
| 7 | before_action :require_sign_in! |
| 8 | before_action :noindex! |
| 9 | |
| 10 | # GET /import/github/connect |
| 11 | # Kicks off the OAuth dance. `visibility=public` narrows the scope to public |
| 12 | # repos only; the default requests `repo` so private repos can be imported. |
| 13 | def connect |
| 14 | unless GithubOauthService.configured? |
| 15 | return redirect_to new_import_path, alert: "GitHub import isn't configured on this server yet." |
| 16 | end |
| 17 | |
| 18 | state = GithubOauthService.generate_state |
| 19 | session[:github_import_state] = state |
| 20 | |
| 21 | include_private = params[:visibility] != "public" |
| 22 | redirect_to GithubOauthService.authorize_url( |
| 23 | redirect_uri: github_import_callback_url, |
| 24 | state: state, |
| 25 | include_private: include_private |
| 26 | ), allow_other_host: true |
| 27 | end |
| 28 | |
| 29 | # GET /import/github/callback |
| 30 | def callback |
| 31 | if params[:error].present? |
| 32 | return redirect_to new_import_path, alert: "GitHub connection was cancelled." |
| 33 | end |
| 34 | |
| 35 | # CSRF: the state we set must round-trip back unchanged. |
| 36 | expected = session.delete(:github_import_state) |
| 37 | if expected.blank? || !ActiveSupport::SecurityUtils.secure_compare(params[:state].to_s, expected) |
| 38 | return redirect_to new_import_path, alert: "GitHub connection failed a security check. Please try again." |
| 39 | end |
| 40 | |
| 41 | token = GithubOauthService.exchange_code( |
| 42 | code: params[:code].to_s, |
| 43 | redirect_uri: github_import_callback_url |
| 44 | ) |
| 45 | |
| 46 | login = GithubApiClient.new(token[:access_token]).login |
| 47 | |
| 48 | connection = current_user.github_connection || current_user.build_github_connection |
| 49 | connection.update!( |
| 50 | access_token: token[:access_token], |
| 51 | scope: token[:scope], |
| 52 | token_type: token[:token_type], |
| 53 | github_login: login, |
| 54 | connected_at: Time.current |
| 55 | ) |
| 56 | |
| 57 | redirect_to new_import_path, notice: "Connected to GitHub#{login ? " as @#{login}" : ''}." |
| 58 | rescue GithubOauthService::ExchangeError, GithubOauthService::ConfigurationError => e |
| 59 | Rails.logger.warn("GitHub import connect failed: #{e.class}") |
| 60 | redirect_to new_import_path, alert: "Couldn't connect to GitHub. Please try again." |
| 61 | end |
| 62 | |
| 63 | # DELETE /import/github/disconnect |
| 64 | def disconnect |
| 65 | current_user.github_connection&.destroy |
| 66 | redirect_to new_import_path, notice: "Disconnected from GitHub." |
| 67 | end |
| 68 | end |