-
Notifications
You must be signed in to change notification settings - Fork 86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GitHub user information #815
Conversation
@@ -0,0 +1,25 @@ | |||
defmodule CodeCorps.GitHub.User do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be made into a struct, for a bit more "typechecking", using defstruct
or even using an ecto embedded schema.
|> cast(params, [:github_auth_token]) | ||
|> validate_required([:github_auth_token]) | ||
|> cast(params, [:github_auth_token, :github_avatar_url, :github_email, :github_id, :github_username]) | ||
|> validate_required([:github_auth_token, :github_avatar_url, :github_id, :github_username]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the amount of fields now, a separate model might not be a terrible idea.
187b245
to
f9f93da
Compare
@joshsmith I'm not sure of the move of
It seems like the file organizing philosophy goes towards having it in |
f6cc0dd
to
dc3143e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@begedin would really love your thoughts on this. Took a first stab at implementing with Bypass
here (been meaning to try it with stripity_stripe
for some time now). I really liked your previous implementation with @behaviour
but wanted to try a slightly different approach that might need less setup going forward. Clearly I have not quite solved that problem, but feel like you might have some insight here after seeing these changes. :)
"access_token" => "foo_auth_token" | ||
} | ||
Plug.Conn.resp(conn, 200, Poison.encode!(response)) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not super happy with how verbose I have to be here. I'd love for this to be reduced to a couple lines here somehow.
"access_token" => "foo_auth_token" | ||
} | ||
Plug.Conn.resp(conn, 200, Poison.encode!(response)) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of this is in violation of DRY, just copied/pasted from below.
config/dev.exs
Outdated
github_oauth_client_id: System.get_env("GITHUB_OAUTH_CLIENT_ID"), | ||
github_oauth_client_secret: System.get_env("GITHUB_OAUTH_CLIENT_SECRET") | ||
github_app_client_id: System.get_env("GITHUB_APP_CLIENT_ID"), | ||
github_app_client_secret: System.get_env("GITHUB_APP_CLIENT_SECRET") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think maybe this could all just be folded into one config rather than duplicated across them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use config.ex
as the base, then override for environments where it needs to be overriden.
@spec get_base_url() :: String.t | ||
defp get_base_url() do | ||
Application.get_env(:code_corps, :github_base_url) || "https://api.github.com/" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Best way I could think to allow us to inject a different Bypass server endpoint, borrowed from an article on Bypass.
lib/code_corps/github.ex
Outdated
# generates JWT with RSA key, using 10 minute expires time, RS256 algo, | ||
# and issuer of the GitHub app's id | ||
"$JWT" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be done still. Not sure best way to handle the RSA key portion, either.
@@ -0,0 +1,43 @@ | |||
defmodule CodeCorps.GitHub.OAuth do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went with this as the place to handle all our top-level OAuth requests.
def retrieve(changes, endpoint, opts) do | ||
changes | ||
|> GitHub.request(:get, endpoint, %{}, opts) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need more functions obviously.
lib/code_corps/github/user.ex
Outdated
def me(access_token, opts \\ []) do | ||
with {:ok, %{"avatar_url" => avatar_url, "email" => email, "id" => id, "login" => login}} <- GitHub.Request.retrieve("user", [access_token: access_token]) | ||
do | ||
{:ok, %{avatar_url: avatar_url, email: email, id: id, login: login}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be simplified with struct
recommendation above.
end | ||
|
||
{:ok, bypass: bypass} | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we could have a different case for this, or some other way to use more quickly this setup
in tests in the future without copying/pasting everywhere.
config/dev.exs
Outdated
github_oauth_client_id: System.get_env("GITHUB_OAUTH_CLIENT_ID"), | ||
github_oauth_client_secret: System.get_env("GITHUB_OAUTH_CLIENT_SECRET") | ||
github_app_client_id: System.get_env("GITHUB_APP_CLIENT_ID"), | ||
github_app_client_secret: System.get_env("GITHUB_APP_CLIENT_SECRET") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use config.ex
as the base, then override for environments where it needs to be overriden.
lib/code_corps/github.ex
Outdated
defstruct [:reason, message: """ | ||
The GitHub HTTP client encountered an error while communicating with | ||
the GitHub API. | ||
"""] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would love if this would work, but it actually means the string will contain an \n
after "with"
. I've been looking for a way to nicely split a string across multiple lines without actually introducing newline characters, and best I could figure out was:
"The GitHub HTTP client encountered an error while communicating with " <>
"the GitHub API."
Note the required space after the "with".
d1f3ec5
to
0c1cf44
Compare
lib/code_corps/github.ex
Outdated
|> Joken.with_signer(Joken.rs256(rsa_key)) | ||
|> Joken.sign | ||
|> Joken.get_compact | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is almost done. What's left is to
lib/code_corps/github.ex
Outdated
"p" => "5cMQg_4MrOnHI44xEs6Jyt_22DCvw3K-GY046Ls50vIf2KlRALHI65SPKfVFo5hUuHkBuWnQV46tHJU0dlmfg4svPMm_581r59yXeI8W6G4FlsSiVyhFO3P5Q5ubVs7MNaqhvaqqPqR14cVvHSqjwX5jGuGAVuLhnOhZGbtb7_U", | ||
"q" => "3RlGNrCRU-yV7TTikKJVJCIpe8vgLBkHQ61iuICd8AyHa4sXICgf2YBFgW8CAJOHKIp8g_Nl94VYpqWvN1YVDB7sFUlRpJL2yXvTKxDzUwtM5pf_D1O6lGEMQBRY-buhZHmPf5qG93LnsSqm5YOZGpZ6t6gHtYM9A6JOIgwsYys", | ||
"qi" => "kG5Stetls18_1fvQx8rxhX2Ais0Xg0gLDUjpE_9TYcb-utq79HVKOQ_2PJGz09hQ_teqnhXhgGMubqaktl6UOSJr6B4JgcAY7yU-34EuSxp8uKLix9BVsF2cpiC4ADhjLKP9c7IQ7X7zfs336_Reb8fh9G_zRdwEfmqFy7m28Lg"} | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is placeholder until we get a .pem file of our own
|
||
@spec handle_response(http_success | http_failure) :: {:ok, map} | {:error, api_error_struct} | ||
defp handle_response({:ok, status, _headers, body}) when status in 200..299 do | ||
decoded_body = body |> Poison.decode! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If were to delay decoding the response and instead send the encoded body out, our individual module endpoints (such as Github.User
could automatically cast as structs:
defmodule Person do
@derive [Poison.Encoder]
defstruct [:name, :age]
end
Poison.encode!(%Person{name: "Devin Torres", age: 27})
#=> "{\"name\":\"Devin Torres\",\"age\":27}"
Poison.decode!(~s({"name": "Devin Torres", "age": 27}), as: %Person{})
I'm not really sure we're gaining enough that way to do it, though
5190f5d
to
88907d7
Compare
I was asked to provide some input on this in the context of separating business logic from the Github API integration. We've been working through similar problem at Timber.io, so I'm happy to share some advice based on our experiences. As you know, some of the initial What does that mean for this? One of the things that I'm doing in some new code for Timber.io is implementing the recommendations of Mocks and Contracts more stringently than I was prior to avoid the situation described above, and I think I can recommend the same thing for you. I'm doing this by separating the line-of-business work from the integration work. For example, retrieving users from GitHub: defmodule CodeCorps.GitHub.User do
defstruct [:avatar_url, :email, :id, :login]
def me(access_token, opts \\ []) do
github_client = get_github_client(opts)
{:ok, user} = github_client.get_user(access_token)
api_response_to_user(user)
end
defp get_github_client(opts) do
# Fetch the GitHub client; if one is not provided in the opts list,
# then get the default from the application configuration, otherwise,
# fallback to the HTTP client
case Keyword.get(opts, :github_client) do
nil ->
Application.get(:code_corps, __MODULE__, [])
|> Keyword.get(:github_client, CodeCorps.GitHub.HTTPClient)
github_client ->
github_client
end
end
end defmodule CodeCorps.GitHub.Client do
@callback get_user(access_token) :: {:ok, map} | {:error, reason}
end defmodule CodeCorps.GitHub.HTTPClient do
@behaviour CodeCorps.Github.Client
## all that fun "real" implementation
end defmodule CodeCorps.GitHub.InMemoryClient do
@behaviour CodeCorps.GitHub.Client
# either make a simple GenServer that allows setting and
# retrieving data or just return static data all the time
end The pros of this are:
The cons are:
What I'm trying to do is improve this further by allowing a function to be passed to defmodule CodeCorps.GitHub.User do
defstruct [:avatar_url, :email, :id, :login]
def me(access_token, opts \\ []) do
get_user = get_implementation(opts, &CodeCorps.GitHub.HTTPClient.get_user/1)
{:ok, user} = get_user.(access_token)
api_response_to_user(user)
end
defp get_implementation(opts, default) do
Keyword.get(opts, :implementation, default)
end
end The code here might seem much more convoluted, but it allows you to do things like this in testing: alias CodeCorps.GitHub.User
test "retrieves user from GitHub " do
user_login = "davidantaramian"
get_user = fn (_) ->
{:ok, %{"login" => user_login, "id" => …}}
end
user = User.me("abcdefg")
assert user.login == user_login
end I hope that this helps you on figuring out a preferred pathway. I recommend loosely coupling wherever possible to avoid testing hell. As of yet, though, I wouldn't say there's a "best way to do it." |
lib/code_corps/github.ex
Outdated
end | ||
|
||
defp rsa_key do | ||
JOSE.JWK.from_pem(@rsa_key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume you meant to use &from_pem_file/1
here.
It's not well documented how &from_pem/1
works. I tried copying the file content into an environment variables in various ways, but none of them worked.
Basically, could you clarify? Do we
- store the contents of the
.pem
file into the environment and use&from_pem/1
- store the path to the
.pem
file into the environment and use&from_pem_file/1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to put a further two cents here, since I'm looking this over. I would also recommend against using a module attribute here (@rsa_key
). This prevents dynamic resolution of the file location. Even running in Docker, just changing an environment variable and restarting the application will not be enough to trigger recompile of the module to pick up the new value. You will either need to launch a new Docker container which compiles the code without any previous build artifacts or force compilation on the existing container.
Instead, prefer functions like the following:
defp get_rsa_file_location() do
Application.get_env(:code_corps, :github_app_pem)
end
This prevents the file location from being "burned in" at build time, since the configuration information is fetched at runtime instead of at build time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We store the contents. You'll see I added a GITHUB_APP_PEM
, which is maybe a misnomer. This will be the contents of the RSA private key, and on Heroku will also be stored as text. I believe Heroku allows up to 32kb of total data in your config vars.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, 32kb total which includes both the keys and their values.
687b4b2
to
b0e1be4
Compare
Add bypass for GitHub
b0e1be4
to
2b10312
Compare
What's in this PR?
Fetches the GitHub user from the GitHub API and save the information to the database.
Still needs: