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