Skip to content

Instantly share code, notes, and snippets.

@mazz
Last active December 12, 2024 04:32
Show Gist options
  • Save mazz/cc35df23923a00c8a049fe690cf9d32e to your computer and use it in GitHub Desktop.
Save mazz/cc35df23923a00c8a049fe690cf9d32e to your computer and use it in GitHub Desktop.
google oauth 400
# ueberauth config
config :ueberauth, Ueberauth,
providers: [
google:
{Ueberauth.Strategy.Google,
[
prompt: "consent",
access_type: "offline",
default_scope: "email"
]}
]
config :ueberauth, Ueberauth.Strategy.Google.OAuth,
client_id: "202717655757-asdfasdf.apps.googleusercontent.com",
client_secret: "asdfasdf"
defmodule Algora.Google do
@moduledoc """
This module contains Google-related functions.
For now, it only contains the function to referesh the user token for the YouTube integration
Perhaps, as time goes on, it'll contain more.
"""
alias GoogleApi.YouTube.V3, as: YouTube
alias Algora.Accounts
alias Algora.Accounts.User
def authorize_url(return_to \\ nil) do
redirect_query = if return_to, do: URI.encode_query(return_to: return_to)
query =
URI.encode_query(
client_id: client_id(),
state: Algora.Util.random_string(),
scope: "user:email",
# redirect_uri: "#{AlgoraWeb.Endpoint.url()}/oauth/callbacks/google?#{redirect_query}"
redirect_uri: AlgoraWeb.Endpoint.url() <> "/auth/google/callback"
)
"https://accounts.google.com/o/oauth2/v2/auth?#{query}"
end
def exchange_access_token(opts) do
code = Keyword.fetch!(opts, :code)
state = Keyword.fetch!(opts, :state)
dbg(code)
dbg(state)
dbg(client_id())
dbg(client_secret())
state
|> fetch_exchange_response(code)
end
# https://accounts.google.com/o/oauth2
defp fetch_exchange_response(state, code) do
dbg(state)
dbg(code)
dbg(client_id())
dbg(client_secret())
dbg(AlgoraWeb.Endpoint.url())
redirect_uri = "#{AlgoraWeb.Endpoint.url()}/auth/google/callback"
dbg(redirect_uri)
resp =
http(
"oauth2.googleapis.com",
"POST",
# "/o/oauth2/token",
"/token",
[
state: state,
code: code,
client_id: client_id(),
client_secret: client_secret(),
# redirect_uri: AlgoraWeb.Endpoint.url() <> "/auth/google",
redirect_uri: redirect_uri,
grant_type: "authorization_code"
],
[{"accept", "application/json"}]
)
with {:ok, resp} <- resp,
%{"access_token" => token} <- Jason.decode!(resp) do
{:ok, token}
else
{:error, _reason} = err -> err
%{} = resp -> {:error, {:bad_response, resp}}
end
end
def upload_video(user = %User{}, path, %{
title: title,
description: description,
privacy_status: privacy_status
}) do
conn = Accounts.get_google_token(user) |> YouTube.Connection.new()
YouTube.Api.Videos.youtube_videos_insert_simple(
conn,
["snippet", "status"],
"multipart",
%YouTube.Model.Video{
snippet: %YouTube.Model.VideoSnippet{
title: title,
description: description
},
status: %YouTube.Model.VideoStatus{
privacyStatus: privacy_status
}
},
path
)
end
defp http(host, method, path, query, headers, body \\ "") do
{:ok, conn} = Mint.HTTP.connect(:https, host, 443)
path = path <> "?" <> URI.encode_query([{:client_id, client_id()} | query])
{:ok, conn, ref} =
Mint.HTTP.request(
conn,
method,
path,
headers,
body
)
receive_resp(conn, ref, nil, nil, false)
end
defp receive_resp(conn, ref, status, data, done?) do
receive do
message ->
{:ok, conn, responses} = Mint.HTTP.stream(conn, message)
{new_status, new_data, done?} =
Enum.reduce(responses, {status, data, done?}, fn
{:status, ^ref, new_status}, {_old_status, data, done?} -> {new_status, data, done?}
{:headers, ^ref, _headers}, acc -> acc
{:data, ^ref, binary}, {status, nil, done?} -> {status, binary, done?}
{:data, ^ref, binary}, {status, data, done?} -> {status, data <> binary, done?}
{:done, ^ref}, {status, data, _done?} -> {status, data, true}
end)
cond do
done? and new_status == 200 -> {:ok, new_data}
done? -> {:error, {new_status, new_data}}
!done? -> receive_resp(conn, ref, new_status, new_data, done?)
end
end
end
defp client_id,
do: Application.fetch_env!(:ueberauth, Ueberauth.Strategy.Google.OAuth)[:client_id]
defp client_secret,
do: Application.fetch_env!(:ueberauth, Ueberauth.Strategy.Google.OAuth)[:client_secret]
end
defmodule AlgoraWeb.OAuthCallbackController do
use AlgoraWeb, :controller
plug(Ueberauth)
require Logger
alias Algora.Accounts
alias Algora.Accounts.User
def new(conn, %{"provider" => "github", "code" => code, "state" => state} = params) do
client = github_client(conn)
with {:ok, info} <- client.exchange_access_token(code: code, state: state),
%{info: info, primary_email: primary, emails: emails, token: token} = info,
{:ok, user} <- Accounts.register_github_user(primary, info, emails, token) do
conn =
if params["return_to"] do
conn |> put_session(:user_return_to, params["return_to"])
else
conn
end
conn
|> put_flash(:info, "Welcome, #{user.handle}!")
|> AlgoraWeb.UserAuth.log_in_user(user)
else
{:error, %Ecto.Changeset{} = changeset} ->
Logger.debug("failed GitHub insert #{inspect(changeset.errors)}")
conn
|> put_flash(
:error,
"We were unable to fetch the necessary information from your GitHub account"
)
|> redirect(to: "/")
{:error, reason} ->
Logger.debug("failed GitHub exchange #{inspect(reason)}")
conn
|> put_flash(:error, "We were unable to contact GitHub. Please try again later")
|> redirect(to: "/")
end
end
def new(conn, %{"provider" => "github", "error" => "access_denied"}) do
redirect(conn, to: "/")
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
# special case used by google #
def callback(
%{assigns: %{ueberauth_auth: auth}} = conn,
%{
"provider" => "google",
"code" => code,
"state" => state
} = params
) do
# user = conn.assigns.current_user
conn =
if params["return_to"] do
conn |> put_session(:user_return_to, params["return_to"])
else
conn
end
dbg(state)
client = google_client(conn)
dbg(client)
case client.exchange_access_token(code: code, state: state) do
{:ok, %{info: info, primary_email: primary, emails: emails, token: token} = info} ->
dbg(info)
{:ok, user} = Accounts.register_google_user(primary, info, emails, token)
conn
|> put_flash(:info, "Google/Youtube account has been linked!")
|> AlgoraWeb.UserAuth.log_in_user(user)
# case User.create_or_update_youtube_identity(user, auth) do
# {:ok, _identity} ->
# # conn
# # |> put_flash(:info, "Successfully connected your YouTube account.")
# # |> redirect(to: "/channel/settings")
# {:error, _changeset} ->
# conn
# |> put_flash(:error, "Error connecting your YouTube account.")
# |> redirect(to: "/channel/settings")
# end
# info
# %{info: info, primary_email: primary, emails: emails, token: token} = info
{:error, description} ->
dbg(description)
conn
|> put_flash(
:error,
"We were unable to fetch the necessary information from your Google account"
)
|> redirect(to: "/")
{:error, %Ecto.Changeset{} = changeset} ->
Logger.debug("failed Google insert #{inspect(changeset.errors)}")
conn
|> put_flash(
:error,
"We were unable to fetch the necessary information from your Google account"
)
|> redirect(to: "/")
{:error, reason} ->
Logger.debug("failed Google exchange #{inspect(reason)}")
conn
|> put_flash(:error, "We were unable to contact Google. Please try again later")
|> redirect(to: "/")
end
end
def callback(%{assigns: %{ueberauth_failure: _failure}} = conn, _params) do
conn
|> put_flash(:error, "Failed to connect YouTube account.")
|> redirect(to: "/channel/settings")
end
defp ensure_authenticated(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must be logged in to connect your YouTube account.")
|> redirect(to: "/auth/login")
|> halt()
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
AlgoraWeb.UserAuth.log_out_user(conn)
end
defp github_client(conn) do
conn.assigns[:github_client] || Algora.Github
end
defp restream_client(conn) do
conn.assigns[:restream_client] || Algora.Restream
end
defp google_client(conn) do
conn.assigns[:google_client] || Algora.Google
end
end
defmodule AlgoraWeb.SignInGoogleLive do
use AlgoraWeb, :live_view
def render(assigns) do
~H"""
<div class="min-h-[calc(100vh-64px)] flex flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-sm max-w-3xl mx-auto mb-[64px] p-12 sm:p-24">
<h2 class="text-center text-3xl font-extrabold text-gray-50">
Algora TV
</h2>
<a
href="https://tyndale-app.fly.dev/auth/google"
class="mt-8 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-400"
>
Sign in with Google
</a>
</div>
</div>
"""
end
def mount(_params, _session, socket) do
{:ok, socket}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment