The most important feature of this app is integration with Strava. To get activities and calculate bikes and parts distance. After 20 posts, I still don’t have it. Of course, I could put all the data manually, but this is the reason to build the app. To make it automatic. I split this to multiple parts. Small pull request are better. It would be easier to follow what happened. And easier to change your mind about how something should work. Strava has really good documentation but there is also good gem for that.

In this part, I want to authorize us in Strava. Flow is simple. We send a request to Strava, they return the URL where user can authorize an account. After that Strava sends a callback to my system. Easy.

I need two endpoints: authorize and callback. In new namespce, with, new app. I start with authorize

require "strava-ruby-client"

module StravaIntegration
  class Controller
    def authorize(request)
      client = ::Strava::OAuth::Client.new(
        client_id: ENV["STRAVA_CLIENT_ID"],
        client_secret: ENV["STRAVA_CLIENT_SECRET"]
      )

      redirect_url = client.authorize_url(
        redirect_uri: ENV["STRAVA_REDIRECT_URI"],
        approval_prompt: "force",
        response_type: "code",
        scope: "activity:read_all",
        state: "magic"
      )
      puts redirect_url
      [302, {"Location" => redirect_url}, []]
    end
  end

  def callback(request)
    client = ::Strava::OAuth::Client.new(
      client_id: ENV["STRAVA_CLIENT_ID"],
      client_secret: ENV["STRAVA_CLIENT_SECRET"]
    )

    response = client.oauth_token(code: request.params["code"])

    tokens = {
      access_token: response.access_token,
      refresh_token: response.refresh_token
    }.to_json

    [302, {"Location" => "/"}, []]
  end
end

Because I don’t have a web interface yet, I test everything with curl I put redirect_url. This allows me to copy URL from rack output and open it in the browser. The whole code looks simple, but there is one tricky part. I have to store tokens in the database. Since I have users in my system I have to recognize who made requests, and in which database I should save them. And right now there is nothing that I could use.

I could make a rule where the system user and strava user have to have the same emails. But it sounds weird, and it going to cause a lot of issues.

I’m going to use the JWT token (the same one I already use) but with a short lifetime.

require "strava-ruby-client"
require "jwt"

module StravaIntegration
  class Controller
    def authorize(request)
      client = ::Strava::OAuth::Client.new(
        client_id: ENV["STRAVA_CLIENT_ID"],
        client_secret: ENV["STRAVA_CLIENT_SECRET"]
      )

      user_token = generate_user_token(request.env["account_id"])

      redirect_url = client.authorize_url(
        redirect_uri: (ENV["STRAVA_REDIRECT_URI"]+"?user_token=#{user_token}"),
        approval_prompt: "force",
        response_type: "code",
        scope: "activity:read_all",
        state: "magic"
      )
      puts redirect_url

      [302, {"Location" => redirect_url}, []]
    end


    def generate_user_token(account_id)
      payload = {
        account_id: account_id,
        exp: (Time.now + 900).to_i,
      }
      JWT.encode(payload, ENV["SECRET_KEY"])
    end

  end
end

As you can see, I’m adding a user token to the callback URL, so recognizing the user should be simple.

def callback(request)
  switch_to_proper_db(request)

  ...
end

def switch_to_proper_db(request)
  token = request.params["user_token"]
  decoded = JWT.decode(token, ENV["SECRET_KEY"])[0]
  tenant_id = decoded["account_id"]
  ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "#{ENV["DB_DIRECTORY"] + ENV["RACK_ENV"]}_#{tenant_id}.sqlite3")
end

At the end I have to save credentials:

Db::Records::StravaCredential.create(
  access_token: response.access_token,
  refresh_token: response.refresh_token
)

After a small refactor, cleaning code, etc. It looks like this:

require "strava-ruby-client"
require "jwt"
require_relative "repository"

module StravaIntegration
  class Controller
    def initialize
      @strava_client = ::Strava::OAuth::Client.new(
        client_id: ENV["STRAVA_CLIENT_ID"],
        client_secret: ENV["STRAVA_CLIENT_SECRET"]
      )
    end

    def authorize(request)
      user_token = generate_user_token(request.env["account_id"])
      redirect_to_strava_authorize_url(user_token)
    end

    def callback(request)
      switch_to_proper_db(request)

      response = exchange_code_for_oauth_token(request.params["code"])

      StravaIntegration::Repository.new.create_credentials(
        access_token: response.access_token, refresh_token: response.refresh_token
      )

      [302, {"Location" => "/"}, []]
    end

    private

    def generate_user_token(account_id)
      payload = {
        account_id: account_id,
        exp: (Time.now + 900).to_i
      }
      JWT.encode(payload, ENV["SECRET_KEY"])
    end

    def switch_to_proper_db(request)
      token = request.params["user_token"]
      decoded = JWT.decode(token, ENV["SECRET_KEY"])[0]
      tenant_id = decoded["account_id"]
      establish_db_connection(tenant_id)
    end

    def redirect_to_strava_authorize_url(user_token)
      redirect_url = @strava_client.authorize_url(
        redirect_uri: "#{ENV["STRAVA_REDIRECT_URI"]}?user_token=#{user_token}",
        approval_prompt: "force",
        response_type: "code",
        scope: "activity:read_all",
        state: "magic"
      )
      puts redirect_url
      [302, {"Location" => redirect_url}, []]
    end

    def exchange_code_for_oauth_token(code)
      @strava_client.oauth_token(code: code)
    end

    def establish_db_connection(tenant_id)
      db_file = "#{ENV["DB_DIRECTORY"]}#{ENV["RACK_ENV"]}_#{tenant_id}.sqlite3"
      ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: db_file)
    end
  end
end

So now I’m authorized in Strava. Tokens are stored in the database. How to make requests and sync data with Strava I’ll show in the next part. As always you can check my code on github