mirror of
https://github.com/algora-io/tv.git
synced 2024-11-16 00:58:59 +02:00
add oauth flow for restream (#37)
This commit is contained in:
parent
5edb301560
commit
8395d38243
@ -11,6 +11,9 @@ AWS_SECRET_ACCESS_KEY=""
|
|||||||
BUCKET_MEDIA=""
|
BUCKET_MEDIA=""
|
||||||
BUCKET_ML=""
|
BUCKET_ML=""
|
||||||
|
|
||||||
|
RESTREAM_CLIENT_ID=""
|
||||||
|
RESTREAM_CLIENT_SECRET=""
|
||||||
|
|
||||||
REPLICATE_API_TOKEN=""
|
REPLICATE_API_TOKEN=""
|
||||||
HF_TOKEN=""
|
HF_TOKEN=""
|
||||||
|
|
||||||
|
@ -10,6 +10,10 @@ config :algora, :github,
|
|||||||
client_id: System.get_env("GITHUB_CLIENT_ID"),
|
client_id: System.get_env("GITHUB_CLIENT_ID"),
|
||||||
client_secret: System.get_env("GITHUB_CLIENT_SECRET")
|
client_secret: System.get_env("GITHUB_CLIENT_SECRET")
|
||||||
|
|
||||||
|
config :algora, :restream,
|
||||||
|
client_id: System.get_env("RESTREAM_CLIENT_ID"),
|
||||||
|
client_secret: System.get_env("RESTREAM_CLIENT_SECRET")
|
||||||
|
|
||||||
config :algora, :event_sink, url: System.get_env("EVENT_SINK_URL")
|
config :algora, :event_sink, url: System.get_env("EVENT_SINK_URL")
|
||||||
|
|
||||||
config :ex_aws,
|
config :ex_aws,
|
||||||
|
@ -94,6 +94,10 @@ if config_env() == :prod do
|
|||||||
client_id: System.fetch_env!("GITHUB_CLIENT_ID"),
|
client_id: System.fetch_env!("GITHUB_CLIENT_ID"),
|
||||||
client_secret: System.fetch_env!("GITHUB_CLIENT_SECRET")
|
client_secret: System.fetch_env!("GITHUB_CLIENT_SECRET")
|
||||||
|
|
||||||
|
config :algora, :restream,
|
||||||
|
client_id: System.fetch_env!("RESTREAM_CLIENT_ID"),
|
||||||
|
client_secret: System.fetch_env!("RESTREAM_CLIENT_SECRET")
|
||||||
|
|
||||||
config :algora, :event_sink, url: System.get_env("EVENT_SINK_URL")
|
config :algora, :event_sink, url: System.get_env("EVENT_SINK_URL")
|
||||||
|
|
||||||
config :ex_aws,
|
config :ex_aws,
|
||||||
|
@ -2,7 +2,7 @@ defmodule Algora.Accounts do
|
|||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Algora.Repo
|
alias Algora.{Repo, Restream}
|
||||||
alias Algora.Accounts.{User, Identity, Destination}
|
alias Algora.Accounts.{User, Identity, Destination}
|
||||||
|
|
||||||
def list_users(opts) do
|
def list_users(opts) do
|
||||||
@ -62,7 +62,7 @@ defmodule Algora.Accounts do
|
|||||||
## User registration
|
## User registration
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Registers a user from their GithHub information.
|
Registers a user from their GitHub information.
|
||||||
"""
|
"""
|
||||||
def register_github_user(primary_email, info, emails, token) do
|
def register_github_user(primary_email, info, emails, token) do
|
||||||
if user = get_user_by_provider_email(:github, primary_email) do
|
if user = get_user_by_provider_email(:github, primary_email) do
|
||||||
@ -74,6 +74,28 @@ defmodule Algora.Accounts do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def link_restream_account(user_id, info, tokens) do
|
||||||
|
user = get_user!(user_id)
|
||||||
|
|
||||||
|
identity =
|
||||||
|
from(u in User,
|
||||||
|
join: i in assoc(u, :identities),
|
||||||
|
where: i.provider == ^to_string(:restream) and u.id == ^user_id
|
||||||
|
)
|
||||||
|
|> Repo.one()
|
||||||
|
|
||||||
|
if identity do
|
||||||
|
update_restream_tokens(user, tokens)
|
||||||
|
else
|
||||||
|
{:ok, _} =
|
||||||
|
info
|
||||||
|
|> Identity.restream_oauth_changeset(user_id, tokens)
|
||||||
|
|> Repo.insert()
|
||||||
|
|
||||||
|
{:ok, Repo.preload(user, :identities, force: true)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def get_user_by_provider_email(provider, email) when provider in [:github] do
|
def get_user_by_provider_email(provider, email) when provider in [:github] do
|
||||||
query =
|
query =
|
||||||
from(u in User,
|
from(u in User,
|
||||||
@ -113,6 +135,29 @@ defmodule Algora.Accounts do
|
|||||||
{:ok, Repo.preload(user, :identities, force: true)}
|
{:ok, Repo.preload(user, :identities, force: true)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp update_restream_tokens(%User{} = user, %{token: token, refresh_token: refresh_token}) do
|
||||||
|
identity =
|
||||||
|
Repo.one!(from(i in Identity, where: i.user_id == ^user.id and i.provider == "restream"))
|
||||||
|
|
||||||
|
{:ok, _} =
|
||||||
|
identity
|
||||||
|
|> change()
|
||||||
|
|> put_change(:provider_token, token)
|
||||||
|
|> put_change(:provider_refresh_token, refresh_token)
|
||||||
|
|> Repo.update()
|
||||||
|
|
||||||
|
{:ok, Repo.preload(user, :identities, force: true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_restream_tokens(%User{} = user) do
|
||||||
|
identity =
|
||||||
|
Repo.one!(from(i in Identity, where: i.user_id == ^user.id and i.provider == "restream"))
|
||||||
|
|
||||||
|
{:ok, tokens} = Restream.refresh_access_token(identity.provider_refresh_token)
|
||||||
|
|
||||||
|
update_restream_tokens(user, tokens)
|
||||||
|
end
|
||||||
|
|
||||||
def gen_stream_key(%User{} = user) do
|
def gen_stream_key(%User{} = user) do
|
||||||
user =
|
user =
|
||||||
Repo.one!(from(u in User, where: u.id == ^user.id))
|
Repo.one!(from(u in User, where: u.id == ^user.id))
|
||||||
|
@ -6,11 +6,13 @@ defmodule Algora.Accounts.Identity do
|
|||||||
|
|
||||||
# providers
|
# providers
|
||||||
@github "github"
|
@github "github"
|
||||||
|
@restream "restream"
|
||||||
|
|
||||||
@derive {Inspect, except: [:provider_token, :provider_meta]}
|
@derive {Inspect, except: [:provider_token, :provider_refresh_token, :provider_meta]}
|
||||||
schema "identities" do
|
schema "identities" do
|
||||||
field :provider, :string
|
field :provider, :string
|
||||||
field :provider_token, :string
|
field :provider_token, :string
|
||||||
|
field :provider_refresh_token, :string
|
||||||
field :provider_email, :string
|
field :provider_email, :string
|
||||||
field :provider_login, :string
|
field :provider_login, :string
|
||||||
field :provider_name, :string, virtual: true
|
field :provider_name, :string, virtual: true
|
||||||
@ -45,4 +47,39 @@ defmodule Algora.Accounts.Identity do
|
|||||||
|> validate_required([:provider_token, :provider_email, :provider_name, :provider_id])
|
|> validate_required([:provider_token, :provider_email, :provider_name, :provider_id])
|
||||||
|> validate_length(:provider_meta, max: 10_000)
|
|> validate_length(:provider_meta, max: 10_000)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
A user changeset for restream oauth.
|
||||||
|
"""
|
||||||
|
def restream_oauth_changeset(info, user_id, %{token: token, refresh_token: refresh_token}) do
|
||||||
|
params = %{
|
||||||
|
"provider_token" => token,
|
||||||
|
"provider_refresh_token" => refresh_token,
|
||||||
|
"provider_id" => to_string(info["id"]),
|
||||||
|
"provider_login" => info["username"],
|
||||||
|
"provider_name" => info["username"],
|
||||||
|
"provider_email" => info["email"],
|
||||||
|
"user_id" => user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
%Identity{provider: @restream, provider_meta: %{"user" => info}}
|
||||||
|
|> cast(params, [
|
||||||
|
:provider_token,
|
||||||
|
:provider_refresh_token,
|
||||||
|
:provider_email,
|
||||||
|
:provider_login,
|
||||||
|
:provider_name,
|
||||||
|
:provider_id,
|
||||||
|
:user_id
|
||||||
|
])
|
||||||
|
|> validate_required([
|
||||||
|
:provider_token,
|
||||||
|
:provider_refresh_token,
|
||||||
|
:provider_email,
|
||||||
|
:provider_name,
|
||||||
|
:provider_id,
|
||||||
|
:user_id
|
||||||
|
])
|
||||||
|
|> validate_length(:provider_meta, max: 10_000)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -5,7 +5,7 @@ defmodule Algora.Github do
|
|||||||
query =
|
query =
|
||||||
URI.encode_query(
|
URI.encode_query(
|
||||||
client_id: client_id(),
|
client_id: client_id(),
|
||||||
state: random_string(),
|
state: Algora.Util.random_string(),
|
||||||
scope: "user:email",
|
scope: "user:email",
|
||||||
redirect_uri: "#{AlgoraWeb.Endpoint.url()}/oauth/callbacks/github?#{redirect_query}"
|
redirect_uri: "#{AlgoraWeb.Endpoint.url()}/oauth/callbacks/github?#{redirect_query}"
|
||||||
)
|
)
|
||||||
@ -82,18 +82,6 @@ defmodule Algora.Github do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def random_string do
|
|
||||||
binary = <<
|
|
||||||
System.system_time(:nanosecond)::64,
|
|
||||||
:erlang.phash2({node(), self()})::16,
|
|
||||||
:erlang.unique_integer()::16
|
|
||||||
>>
|
|
||||||
|
|
||||||
binary
|
|
||||||
|> Base.url_encode64()
|
|
||||||
|> String.replace(["/", "+"], "-")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp client_id, do: Algora.config([:github, :client_id])
|
defp client_id, do: Algora.config([:github, :client_id])
|
||||||
defp secret, do: Algora.config([:github, :client_secret])
|
defp secret, do: Algora.config([:github, :client_secret])
|
||||||
|
|
||||||
|
90
lib/algora/restream.ex
Normal file
90
lib/algora/restream.ex
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
defmodule Algora.Restream do
|
||||||
|
def authorize_url(state) do
|
||||||
|
query =
|
||||||
|
URI.encode_query(
|
||||||
|
client_id: client_id(),
|
||||||
|
state: state,
|
||||||
|
response_type: "code",
|
||||||
|
redirect_uri: redirect_uri()
|
||||||
|
)
|
||||||
|
|
||||||
|
"https://api.restream.io/login?#{query}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def exchange_access_token(opts) do
|
||||||
|
code = Keyword.fetch!(opts, :code)
|
||||||
|
state = Keyword.fetch!(opts, :state)
|
||||||
|
|
||||||
|
state
|
||||||
|
|> fetch_exchange_response(code)
|
||||||
|
|> fetch_user_info()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_exchange_response(_state, code) do
|
||||||
|
body =
|
||||||
|
URI.encode_query(%{
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
redirect_uri: redirect_uri(),
|
||||||
|
code: code
|
||||||
|
})
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
{"Content-Type", "application/x-www-form-urlencoded"},
|
||||||
|
{"Authorization", "Basic " <> Base.encode64("#{client_id()}:#{secret()}")}
|
||||||
|
]
|
||||||
|
|
||||||
|
resp = HTTPoison.post("https://api.restream.io/oauth/token", body, headers)
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- resp,
|
||||||
|
%{"access_token" => token, "refresh_token" => refresh_token} <- Jason.decode!(body) do
|
||||||
|
{:ok, %{token: token, refresh_token: refresh_token}}
|
||||||
|
else
|
||||||
|
{:error, %HTTPoison.Error{reason: reason}} -> {:error, reason}
|
||||||
|
%{} = resp -> {:error, {:bad_response, resp}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_user_info({:error, _reason} = error), do: error
|
||||||
|
|
||||||
|
defp fetch_user_info({:ok, %{token: token} = tokens}) do
|
||||||
|
headers = [{"Authorization", "Bearer #{token}"}]
|
||||||
|
|
||||||
|
case HTTPoison.get("https://api.restream.io/v2/user/profile", headers) do
|
||||||
|
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||||
|
{:ok, %{info: Jason.decode!(body), tokens: tokens}}
|
||||||
|
|
||||||
|
{:ok, %HTTPoison.Response{status_code: status_code, body: body}} ->
|
||||||
|
{:error, {status_code, body}}
|
||||||
|
|
||||||
|
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_access_token(refresh_token) do
|
||||||
|
body =
|
||||||
|
URI.encode_query(%{
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: refresh_token
|
||||||
|
})
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
{"Content-Type", "application/x-www-form-urlencoded"},
|
||||||
|
{"Authorization", "Basic " <> Base.encode64("#{client_id()}:#{secret()}")}
|
||||||
|
]
|
||||||
|
|
||||||
|
resp = HTTPoison.post("https://api.restream.io/oauth/token", body, headers)
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- resp,
|
||||||
|
%{"access_token" => token, "refresh_token" => refresh_token} <- Jason.decode!(body) do
|
||||||
|
{:ok, %{token: token, refresh_token: refresh_token}}
|
||||||
|
else
|
||||||
|
{:error, %HTTPoison.Error{reason: reason}} -> {:error, reason}
|
||||||
|
%{} = resp -> {:error, {:bad_response, resp}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp client_id, do: Algora.config([:restream, :client_id])
|
||||||
|
defp secret, do: Algora.config([:restream, :client_secret])
|
||||||
|
defp redirect_uri, do: "#{AlgoraWeb.Endpoint.url()}/oauth/callbacks/restream"
|
||||||
|
end
|
@ -83,4 +83,16 @@ defmodule Algora.Util do
|
|||||||
]
|
]
|
||||||
|
|
||||||
def is_common_word(s), do: Enum.member?(@common_words, s)
|
def is_common_word(s), do: Enum.member?(@common_words, s)
|
||||||
|
|
||||||
|
def random_string do
|
||||||
|
binary = <<
|
||||||
|
System.system_time(:nanosecond)::64,
|
||||||
|
:erlang.phash2({node(), self()})::16,
|
||||||
|
:erlang.unique_integer()::16
|
||||||
|
>>
|
||||||
|
|
||||||
|
binary
|
||||||
|
|> Base.url_encode64()
|
||||||
|
|> String.replace(["/", "+"], "-")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -27,7 +27,7 @@ defmodule AlgoraWeb.OAuthCallbackController do
|
|||||||
conn
|
conn
|
||||||
|> put_flash(
|
|> put_flash(
|
||||||
:error,
|
:error,
|
||||||
"We were unable to fetch the necessary information from your GithHub account"
|
"We were unable to fetch the necessary information from your GitHub account"
|
||||||
)
|
)
|
||||||
|> redirect(to: "/")
|
|> redirect(to: "/")
|
||||||
|
|
||||||
@ -44,6 +44,46 @@ defmodule AlgoraWeb.OAuthCallbackController do
|
|||||||
redirect(conn, to: "/")
|
redirect(conn, to: "/")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new(conn, %{"provider" => "restream", "code" => code, "state" => state}) do
|
||||||
|
client = restream_client(conn)
|
||||||
|
|
||||||
|
user_id = get_session(conn, :user_id)
|
||||||
|
|
||||||
|
with {:ok, state} <- verify_session(conn, :restream_state, state),
|
||||||
|
{:ok, info} <- client.exchange_access_token(code: code, state: state),
|
||||||
|
%{info: info, tokens: tokens} = info,
|
||||||
|
{:ok, user} <- Accounts.link_restream_account(user_id, info, tokens) do
|
||||||
|
conn
|
||||||
|
|> put_flash(:info, "Restream account has been linked!")
|
||||||
|
|> AlgoraWeb.UserAuth.log_in_user(user)
|
||||||
|
else
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
Logger.debug("failed Restream insert #{inspect(changeset.errors)}")
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_flash(
|
||||||
|
:error,
|
||||||
|
"We were unable to fetch the necessary information from your Restream account"
|
||||||
|
)
|
||||||
|
|> redirect(to: "/")
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.debug("failed Restream exchange #{inspect(reason)}")
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "We were unable to contact Restream. Please try again later")
|
||||||
|
|> redirect(to: "/")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp verify_session(conn, key, token) do
|
||||||
|
if Plug.Crypto.secure_compare(token, get_session(conn, key)) do
|
||||||
|
{:ok, token}
|
||||||
|
else
|
||||||
|
{:error, "#{key} is invalid"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def sign_out(conn, _) do
|
def sign_out(conn, _) do
|
||||||
AlgoraWeb.UserAuth.log_out_user(conn)
|
AlgoraWeb.UserAuth.log_out_user(conn)
|
||||||
end
|
end
|
||||||
@ -51,4 +91,8 @@ defmodule AlgoraWeb.OAuthCallbackController do
|
|||||||
defp github_client(conn) do
|
defp github_client(conn) do
|
||||||
conn.assigns[:github_client] || Algora.Github
|
conn.assigns[:github_client] || Algora.Github
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp restream_client(conn) do
|
||||||
|
conn.assigns[:restream_client] || Algora.Restream
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
17
lib/algora_web/controllers/oauth_login_controller.ex
Normal file
17
lib/algora_web/controllers/oauth_login_controller.ex
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
defmodule AlgoraWeb.OAuthLoginController do
|
||||||
|
use AlgoraWeb, :controller
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def new(conn, %{"provider" => "restream"} = params) do
|
||||||
|
if conn.assigns.current_user do
|
||||||
|
state = Algora.Util.random_string()
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_session(:user_return_to, params["return_to"])
|
||||||
|
|> put_session(:restream_state, state)
|
||||||
|
|> redirect(external: Algora.Restream.authorize_url(state))
|
||||||
|
else
|
||||||
|
conn |> redirect(to: ~p"/auth/login")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -80,6 +80,21 @@ defmodule AlgoraWeb.SettingsLive do
|
|||||||
<.button phx-click="show_add_destination_modal">Add Destination</.button>
|
<.button phx-click="show_add_destination_modal">Add Destination</.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div :if={Mix.env() == :dev} class="space-y-6 bg-white/5 rounded-lg p-6 ring-1 ring-white/15">
|
||||||
|
<.header>
|
||||||
|
Integrations
|
||||||
|
<:subtitle>
|
||||||
|
Connect with other apps
|
||||||
|
</:subtitle>
|
||||||
|
</.header>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<.button>
|
||||||
|
<.link href={"/oauth/login/restream?#{URI.encode_query(return_to: "/channel/settings")}"}>
|
||||||
|
Restream
|
||||||
|
</.link>
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Add Destination Modal -->
|
<!-- Add Destination Modal -->
|
||||||
<.modal :if={@show_add_destination_modal} id="add-destination-modal" show>
|
<.modal :if={@show_add_destination_modal} id="add-destination-modal" show>
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
defmodule AlgoraWeb.Router do
|
defmodule AlgoraWeb.Router do
|
||||||
use AlgoraWeb, :router
|
use AlgoraWeb, :router
|
||||||
|
|
||||||
import AlgoraWeb.UserAuth,
|
import AlgoraWeb.UserAuth, only: [fetch_current_user: 2]
|
||||||
only: [redirect_if_user_is_authenticated: 2, fetch_current_user: 2]
|
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html"]
|
plug :accepts, ["html"]
|
||||||
@ -23,9 +22,10 @@ defmodule AlgoraWeb.Router do
|
|||||||
end
|
end
|
||||||
|
|
||||||
scope "/", AlgoraWeb do
|
scope "/", AlgoraWeb do
|
||||||
pipe_through [:browser, :redirect_if_user_is_authenticated]
|
pipe_through :browser
|
||||||
|
|
||||||
get "/oauth/callbacks/:provider", OAuthCallbackController, :new
|
get "/oauth/callbacks/:provider", OAuthCallbackController, :new
|
||||||
|
get "/oauth/login/:provider", OAuthLoginController, :new
|
||||||
end
|
end
|
||||||
|
|
||||||
if Mix.env() in [:dev, :test] do
|
if Mix.env() in [:dev, :test] do
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
defmodule Algora.Repo.Local.Migrations.AddProviderRefreshTokenToIdentities do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table("identities") do
|
||||||
|
add :provider_refresh_token, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user