# frozen_string_literal: true require_relative "../ui" require "net/http" require "uri" require "json" require "launchy" require "fileutils" module Neetob class CLI module Github class Auth attr_accessor :client_id, :grant_type, :uris, :provider, :scope, :access_token, :ui def initialize(client_id:, grant_type:, auth_uris:, provider:, scope:) @client_id = client_id @grant_type = grant_type @uris = auth_uris @provider = provider @scope = scope @access_token = retrieve_persisted_token @ui = CLI::UI.new end def token_persisted? access_token_present? end def open_url_in_browser!(url) Launchy.open(url) do |exception| raise(StandardError, "Attempted to open #{url} in browser and failed because #{exception}.") end end def start_oauth2_device_flow auth_data = request_authorization show_user_code(auth_data[:user_code]) open_url_in_browser!(auth_data[:verification_uri]) poll_for_token(auth_data) end def request_authorization post(uris["auth_req"], params: { client_id: client_id, scope: scope }) end private def show_user_code(code) ui.say("You'll be redirected to the browser in 5 secs. Enter the following code in the browser:") ui.success(code) sleep 5 end def tmp_token_path(provider) "/tmp/neetob_#{provider}_token" end def provider_local_token_path @_provider_local_token_path ||= tmp_token_path(provider) end def retrieve_persisted_token File.read(provider_local_token_path) if File.file?(provider_local_token_path) end def parse_response(http_result) case http_result when Net::HTTPOK JSON.parse(http_result.body, { symbolize_names: true }) else raise(StandardError, "Request failed with status code #{http_result.code}. #{http_result.body}") nil end end def post(url, params:, headers: { "Accept" => "application/json" }) base_uri = URI(url) enc_params = URI.encode_www_form(params) http_result = Net::HTTP.post(base_uri, enc_params, headers) parse_response(http_result) end def poll_for_token(auth_data) interval = auth_data[:interval] loop do res = post( uris["token_req"], params: { client_id: client_id, device_code: auth_data[:device_code], grant_type: grant_type } ) if res.key?(:error) case res[:error] when "authorization_pending" # dont hit the endpoint too fast such that we get rate limited sleep interval next when "slow_down" # increase polling interval incase rate limited interval = res["interval"] sleep interval next when "access_denied" # user clicks cancel button raise(StandardError, "Access denied while authorizing #{provider} access.") break else raise(StandardError, res) break end end # We won't be running this script regularly. Thus storing in /tmp/ # for temporary reuse of token till machine is rebooted File.write(provider_local_token_path, res[:access_token]) FileUtils.chmod(0600, provider_local_token_path) self.access_token = res[:access_token] ui.info("You've been authenticated!") break end end def access_token_present? !(access_token.nil? || access_token.empty?) end end end end end