During your journey as an Elixir developer, there is going to come to a point where you might want to publish your own package. This has happened to me a couple of times already, here are a couple of packages I have published.
Equip with the knowledge of what I needed to do to publish my own package, I thought I would take you on the same journey. Let's get started.
Our first stop is going to be the Hex website. Hex has a great guide for publishing your own package that we will be following.
Registering a Hex User
Before we can do anything, we will need to register our own user. When registering a user, you will be prompted for a username, your email, and a password. The email is used to confirm your identity during signup, as well as to contact you in case there is an issue with one of your packages. The email will never be shared with a third party.
$ mix hex.user register
Now it’s time to decide on a name for your package. In this guide I will be creating a new Ueberauth package. If you were to go on http://hex.pm and look at other Ueberauth packages, you notice there is a certain pattern followed. This will make the decision easy for us on what to call our Uberauth package that will implement the Patreon OAuth flow.
uberauth_patreon
Generating Our Project
Now that we know the name of our package let’s generate the project
If you’re curious how to generate a standard mix project, please visit the Elixir site here: https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html#our-first-project
mix new ueberauth_patreon
We should expect to see:
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/ueberauth_patreon.ex
* creating test
* creating test/test_helper.exs
* creating test/ueberauth_patreon_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd ueberauth_patreon
mix test
Run "mix help" for more commands.
Let’s change the directory to our new project directory and let’s open the code in your editor of choice. For me I’ll be using Visual Studio Code, this is important later in the guide when I am making a test app.
Next, we will update the mix.exs file to have all the settings we know to start things off. I follow this section of the Hex publishing guide https://hex.pm/docs/publish#adding-metadata-to-code-classinlinemixexscode
def project do
[
app: :ueberauth_patreon,
description: "Ueberauth strategy for Patreon OAuth.",
version: "1.0.0",
elixir: "~> 1.13",
source_url: "https://github.com/talk2MeGooseman/ueberauth_patreon",
homepage_url: "https://github.com/talk2MeGooseman/ueberauth_patreon",
start_permanent: Mix.env() == :prod,
deps: deps(),
package: [
links: %{"GitHub" => "https://github.com/talk2MeGooseman/ueberauth_patreon"},
licenses: ["MIT"],
]
]
end
I decided to set my package version at 1.0.0
but feel free to start it at 0.0.1
since its your first version ever.
Adding Documentation Generation
If you follow along in the Hex guide, they will suggest using ex_doc
and there is no reason not to. It generates some beautiful documentation.
Following the HexDoc’s guide https://hexdocs.pm/ex_doc/readme.html. Let’s add it to our project dependencies in our mix.exs
file.
def deps do
[
## Other deps ...
{:ex_doc, "~> 0.27", only: :dev, runtime: false},
]
end
Dont forget to install our new dependency.
mix deps.get
Now let's generate our docs to make sure everything is set up correctly
> mix docs
Generating docs...
View "html" docs at "doc/index.html"
View "epub" docs at "doc/ueberauth_patreon.epub"
If you have the latest NPM installed you can run the following command to see your docs in the browser:
npx serve doc/
This will kick off a little webserver without having to go install anything locally.
Let’s Get Coding
When creating a Ueberauth package, the project needs to be structured in a particular way. If you click on any of the listed strategies Ueberauth has you will get a lot of good examples of what you need to do. You can structure your project any way you like but in the case of Ueberauth it’s best we follow their expected structure.
https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies
Inside of lib
, we need to create several files and folders.
mkdir lib/ueberauth
mkdir lib/ueberauth/strategy
touch lib/ueberauth/strategy/patreon.ex
mkdir lib/ueberauth/strategy/patreon
touch lib/ueberauth/strategy/patreon/oauth.ex
Since we're making an Ueberauth strategy we need to make sure to install Ueberauth and OAuth2 packages. The mix.exs
file should start looking like this now.
defp deps do
[
{:ueberauth, "~> 0.7"},
{:oauth2, "~> 2.0"},
{:ex_doc, "~> 0.27", only: :dev, runtime: false}
]
end
Now let’s install those new dependencies.
mix deps.get
Now it's time to implement what we want our package to do. I am not going to explain how to implement an Uberauth package in this article, you can read my tutorial Creating Your Own Ueberauth Strategy on how. Instead, we'll just be copying and pasting some code in.
Inside lib/ueberauth/strategy/patreon.ex
add the following:
defmodule Ueberauth.Strategy.Patreon do
use Ueberauth.Strategy,
oauth2_module: Ueberauth.Strategy.Patreon.OAuth
alias Ueberauth.Auth.Info
alias Ueberauth.Auth.Credentials
alias Ueberauth.Auth.Extra
@doc """
Handles the initial redirect to the patreon authentication page.
To customize the scope (permissions) that are requested by patreon include
them as part of your url:
"https://www.patreon.com/oauth2/authorize"
"""
def handle_request!(conn) do
scopes = conn.params["scope"] || option(conn, :default_scope)
params =
[scope: scopes]
|> with_state_param(conn)
module = option(conn, :oauth2_module)
redirect!(conn, apply(module, :authorize_url!, [params]))
end
@doc """
Handles the callback from Patreon.
When there is a failure from Patreon the failure is included in the
`ueberauth_failure` struct. Otherwise the information returned from Patreon is
returned in the `Ueberauth.Auth` struct.
"""
def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do
module = option(conn, :oauth2_module)
token = apply(module, :get_token!, [[code: code]])
if token.access_token == nil do
set_errors!(conn, [
error(token.other_params["error"], token.other_params["error_description"])
])
else
fetch_user(conn, token)
end
end
@doc false
def handle_callback!(conn) do
set_errors!(conn, [error("missing_code", "No code received")])
end
@doc """
Cleans up the private area of the connection used for passing the raw Notion
response around during the callback.
"""
def handle_cleanup!(conn) do
conn
|> put_private(:patreon_token, nil)
|> put_private(:patreon_user, nil)
end
@doc """
Fetches the uid field from the Twitch response. This defaults to the option `uid_field` which in-turn defaults to `id`
"""
def uid(conn) do
%{"data" => user} = conn.private.patreon_user
user["id"]
end
@doc """
Includes the credentials from the Patreon response.
"""
def credentials(conn) do
token = conn.private.patreon_token
%Credentials{
token: token.access_token,
token_type: token.token_type,
refresh_token: token.refresh_token,
expires_at: token.expires_at
}
end
@doc """
Fetches the fields to populate the info section of the `Ueberauth.Auth`
struct.
"""
def info(conn) do
%{ "data" => %{
"attributes" => %{
"full_name" => full_name,
"first_name" => first_name,
"last_name" => last_name,
"about" => about,
"image_url" => image_url,
"url" => url,
"email" => email
}
}} = conn.private.patreon_user
%Info{
email: email,
name: full_name,
first_name: first_name,
last_name: last_name,
description: about,
image: image_url,
urls: %{
profile: url
}
}
end
@doc """
Stores the raw information (including the token) obtained from the Patreon
callback.
"""
def extra(conn) do
%Extra{
raw_info: conn.private.patreon_user
}
end
defp fetch_user(conn, token) do
conn = put_private(conn, :patreon_token, token)
case Ueberauth.Strategy.Patreon.OAuth.get(
token.access_token,
"https://www.patreon.com/api/oauth2/v2/identity?fields%5Buser%5D=full_name,email,first_name,last_name,about,image_url,url"
) do
{:ok, %OAuth2.Response{status_code: 401, body: _body}} ->
set_errors!(conn, [error("token", "unauthorized")])
{:ok, %OAuth2.Response{status_code: status_code, body: user}}
when status_code in 200..399 ->
put_private(conn, :patreon_user, user)
{:error, %OAuth2.Error{reason: reason}} ->
set_errors!(conn, [error("OAuth2", reason)])
{:error, %OAuth2.Response{body: %{"message" => reason}}} ->
set_errors!(conn, [error("OAuth2", reason)])
{:error, _} ->
set_errors!(conn, [error("OAuth2", "uknown error")])
end
end
defp option(conn, key) do
Keyword.get(options(conn), key, Keyword.get(default_options(), key))
end
end
Inside lib/ueberauth/strategy/patreon/oauth.ex
paste the following code:
defmodule Ueberauth.Strategy.Patreon.OAuth do
@moduledoc """
An implementation of OAuth2 for Patreon.
To add your `:client_id` and `:client_secret` include these values in your
configuration:
config :ueberauth, Ueberauth.Strategy.Patreon.OAuth,
client_id: System.get_env("PATREON_CLIENT_ID"),
client_secret: System.get_env("PATREON_CLIENT_SECRET")
"""
use OAuth2.Strategy
@defaults [
strategy: __MODULE__,
site: "https://www.patreon.com/",
authorize_url: "https://www.patreon.com/oauth2/authorize",
token_url: "https://www.patreon.com/api/oauth2/token",
token_method: :post
]
@doc """
Construct a client for requests to Patreon.
Optionally include any OAuth2 options here to be merged with the defaults:
Ueberauth.Strategy.Patreon.OAuth.client(
redirect_uri: "http://localhost:4000/auth/patreon/callback"
)
This will be setup automatically for you in `Ueberauth.Strategy.Patreon`.
These options are only useful for usage outside the normal callback phase of
Ueberauth.
"""
def client(opts \\ []) do
config =
:ueberauth
|> Application.fetch_env!(Ueberauth.Strategy.Patreon.OAuth)
|> check_credential(:client_id)
|> check_credential(:client_secret)
|> check_credential(:redirect_uri)
client_opts =
@defaults
|> Keyword.merge(config)
|> Keyword.merge(opts)
json_library = Ueberauth.json_library()
OAuth2.Client.new(client_opts)
|> OAuth2.Client.put_serializer("application/json", json_library)
|> OAuth2.Client.put_serializer("application/vnd.api+json", json_library)
end
@doc """
Provides the authorize url for the request phase of Ueberauth.
No need to call this usually.
"""
def authorize_url!(params \\ [], opts \\ []) do
opts
|> client
|> OAuth2.Client.authorize_url!(params)
end
def get(token, url, headers \\ [], opts \\ []) do
client()
|> put_header("authorization", "Bearer " <> token)
|> put_header("accept", "application/json")
|> OAuth2.Client.get(url, headers, opts)
end
def get_token!(params \\ [], options \\ []) do
headers = Keyword.get(options, :headers, [])
options = Keyword.get(options, :options, [])
client_options = Keyword.get(options, :client_options, [])
client = OAuth2.Client.get_token!(client(client_options), params, headers, options)
client.token
end
# Strategy Callbacks
def authorize_url(client, params) do
OAuth2.Strategy.AuthCode.authorize_url(client, params)
end
def get_token(client, params, headers) do
client =
client
|> put_param("grant_type", "authorization_code")
|> put_header("Accept", "application/json")
OAuth2.Strategy.AuthCode.get_token(client, params, headers)
end
defp check_credential(config, key) do
check_config_key_exists(config, key)
case Keyword.get(config, key) do
value when is_binary(value) ->
config
{:system, env_key} ->
case System.get_env(env_key) do
nil ->
raise "#{inspect(env_key)} missing from environment, expected in config :ueberauth, Ueberauth.Strategy.Patreon"
value ->
Keyword.put(config, key, value)
end
end
end
defp check_config_key_exists(config, key) when is_list(config) do
unless Keyword.has_key?(config, key) do
raise "#{inspect(key)} missing from config :ueberauth, Ueberauth.Strategy.Patreon"
end
config
end
defp check_config_key_exists(_, _) do
raise "Config :ueberauth, Ueberauth.Strategy.Patreon is not a keyword list, as expected"
end
end
It’s always a great idea to add doc headers to all your public functions you expect your package users to be using. This will help them immensely in figuring out what each function does and seeing examples of input and output. For this package, a user won't be using it directly but instead following specific setup steps, so our documentation reflects that.
Generate our documentation to see how things look now.
mix docs
And spin up a server to see how things.
npx serve docs/
Bonus: Add the README
This is more of a personal preference than anything else. I'm not sure if you noticed when browsing the Hex Docs for various packages. But sometimes you will come across a project that has pretty nice docs but the setup steps will be missing. If you actually visit the packages repository, you will find a great README with all the information on how to setup up the project.
GAHHHH I also need those steps in those docs! Well, you easily can tell ex_doc
to include the README. Let’s update our project settings to do just that.
In our mix.exs
file add the docs
field, it should look something like this now.
def project do
[
app: :ueberauth_patreon,
description: "Ueberauth strategy for Patreon OAuth.",
version: "1.0.0",
elixir: "~> 1.13",
source_url: "https://github.com/talk2MeGooseman/ueberauth_patreon",
homepage_url: "https://github.com/talk2MeGooseman/ueberauth_patreon",
start_permanent: Mix.env() == :prod,
deps: deps(),
package: [
links: %{"GitHub" => "https://github.com/talk2MeGooseman/ueberauth_patreon"},
licenses: ["MIT"],
],
docs: [ #### New docs field
extras: ["README.md"] ### This is going to include the readme in the docs
]
]
end
Now let’s go to our README and update it with the project setup steps.
# Überauth Patreon
[![Hex Version](https://cdn.hashnode.com/res/hashnode/image/upload/v1679633621626/fe2dc713-2811-4631-8d98-6abd29a2d38b.svg)](https://hex.pm/packages/ueberauth_patreon)
> Patreon OAuth2 strategy for Überauth.
## Installation
1. Setup your application in Patreon Development Dashboard https://www.patreon.com/portal/registration/register-clients
1. Add `:ueberauth_patreon` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:ueberauth_patreon, "~> 1.0"}]
end
Add Patreon to your Überauth configuration:
config :ueberauth, Ueberauth, providers: [ patreon: {Ueberauth.Strategy.Patreon, [default_scope: "identity[email] identity"]}, ]
Update your provider configuration:
config :ueberauth, Ueberauth.Strategy.Patreon.OAuth, client_id: System.get_env("PATREON_CLIENT_ID"), client_secret: System.get_env("PATREON_CLIENT_SECRET"), redirect_uri: System.get_env("PATREON_REDIRECT_URI")
Include the Überauth plug in your router pipeline:
defmodule TestPatreonWeb.Router do use TestPatreonWeb, :router pipeline :browser do plug Ueberauth ... end end
Add the request and callback routes:
scope "/auth", TestPatreonWeb do pipe_through :browser get "/:provider", AuthController, :request get "/:provider/callback", AuthController, :callback end
Create a new controller or use an existing controller that implements callbacks to deal with
Ueberauth.Auth
andUeberauth.Failure
responses from Patreon.defmodule TestPatreonWeb.AuthController do use TestPatreonWeb, :controller def callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do conn |> put_flash(:error, "Failed to authenticate.") |> redirect(to: "/") end def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do case UserFromAuth.find_or_create(auth) do {:ok, user} -> conn |> put_flash(:info, "Successfully authenticated.") |> put_session(:current_user, user) |> configure_session(renew: true) |> redirect(to: "/") {:error, reason} -> conn |> put_flash(:error, reason) |> redirect(to: "/") end end end
Calling
Once your setup, you can initiate auth using the following URL, unless you changed the routes from the guide:
/auth/patreon
Documentation
The docs can be found at ueberauth_patreon on Hex Docs.
Generate our documentation to see how things look now.
```bash
mix docs
And spin up a server to see how things.
npx serve docs/
Our new package is code complete but we don't know if it actually works.
Testing Our Package In A Project
I think I write half-decent code, but not enough to trust that it just works so I want to try testing it in an actual Phoenix project. Let’s spin one up to test it LIVE.
mix phx.new test_patreon
Short Detour: Using VS Code Run Our Project
We will need a database for our project, the easiest way I like to do this is using Visual Studio Code’s Remote - Containers feature. Using Docker and an existing template, I can spin up a dev environment that all the fixings I need to work on a Phoenix project.
Start VS Code, run the Remote-Containers: Open Folder in Container... command from the Command Palette (F1) or quick actions Status bar item, and select the project folder you would like to set up the container for.
Now select Show All Definitions... > Elixir, Phoenix, Node.js & PostgresSQL (Community)
After picking the starting point for your container, VS Code will add the dev container configuration files to your project (
.devcontainer/devcontainer.json
).The VS Code window will reload and start building the dev container. A progress notification provides status updates. You only have to build a dev container the first time you open it; opening the folder after the first successful build will be much quicker.
After the build completes, VS Code will automatically connect to the container.
Now your project is all set up with everything you need to run the project. You will most likely need to go into the docker-compose.yml
file and update the database name to the name your project will be using.
Resuming: Testing Our Package In A Project
Let make sure our project setup works.
mix setup
In order to test our project, we don’t need to worry about publishing it to Hex. We can install it locally or install it through git. I have already pushed my project to Github so I want to show you how you can install any package hosted on Git or Github really easily.
Add the following to your deps. Your name or URL would be different if your project.
{:ueberauth_patreon, github: "talk2MeGooseman/ueberauth_patreon"}
As a personal rule of thumb, make sure to go through the setup guide you have for your package VERBATIM. This ensures you didn't miss any steps as part of the setup guide, because were the author sometimes we can accidentally gloss over an important.
Now install your package and test it out.
mix deps.get
Here is to hoping your project just works during testing it!
UH OH, My Package Has A Bug!
When the inevitable and your find a bug in your code there are a couple of things you can do.
- Make updates to your package project code and push the changes to Git. If you do this, in your test project make sure to run the following command to install the latest updates.
mix deps.update ueberauth_patreon
- Modify the package code directly inside your test project. All your installed packages can be found inside your
deps/
folder and any changes you make to them can be recompiled so you can see how they affect your project. This can save you the trouble of having to go back to your package project, make code changes, and then push the code. Just run the following command to recompile your package.
mix deps.compile ueberauth_patreon
Make sure you copy the fixes you make back to your package project and commit them!
Submitting The Package
Now a package is complete and is ready to publish. Hex again does a great job detailing the steps you need to go through here https://hex.pm/docs/publish#submitting-the-package
Just run the command and confirm when you verify everything looks good.
> mix hex.publish
Building ueberauth_patreon 1.0.0
Dependencies:
ueberauth ~> 0.7 (app: ueberauth)
oauth2 ~> 2.0 (app: oauth2)
App: ueberauth_patreon
Name: ueberauth_patreon
Files:
lib
lib/ueberauth
lib/ueberauth/strategy
lib/ueberauth/strategy/patreon
lib/ueberauth/strategy/patreon/oauth.ex
lib/ueberauth/strategy/patreon.ex
lib/ueberauth_patreon.ex
.formatter.exs
mix.exs
README.md
Version: 1.0.0
Build tools: mix
Description: Ueberauth strategy for Patreon OAuth.
Licenses: MIT
Links:
GitHub: https://github.com/talk2MeGooseman/ueberauth_patreon
Elixir: ~> 1.13
Before publishing, please read the Code of Conduct: https://hex.pm/policies/codeofconduct
Publishing package to public repository hexpm.
Oh No I Messed Up My Docs!
Free not, for Hex has a way to directly publish doc updates. No need to make a new release just to update docs. Once you have made your docs updates just run the following command and you are good.
mix hex.publish docs