defmodule BrightWeb.AuthController do use BrightWeb, :controller use BrightWeb, :verified_routes alias Bright.Users alias Bright.Users.User alias Bright.Patrons.TierMapper alias Bright.Repo require Logger plug(Ueberauth) import Plug.Conn import Phoenix.Controller alias Phoenix.LiveView def delete(conn, _params) do conn |> clear_session() |> redirect(to: ~p"/") end def callback(conn = %{assigns: %{ueberauth_auth: auth}}, _params) do Logger.debug("callback() with ueberauth_auth defined. Let's get a User!") Logger.debug(inspect(auth)) if user = Users.get_by_ueberauth(auth) do Logger.debug(">>>>>> user has been here before") Logger.debug("user=#{inspect(user)}") Logger.debug("auth=#{inspect(auth)}") ## @todo here we need to update the user's patron_tier attrs = params_from_ueberauth(auth) case Users.update_user(user, attrs) do {:ok, user} -> conn |> sign_in_and_redirect(user, ~p"/profile") {:error, reason} -> Logger.error("error while updating the user's patron_tier. #{inspect(reason)}") conn |> put_flash(:error, "Patreon tier synchronization failed. Please try again.") |> redirect(to: ~p"/") end else Logger.debug(">>>>>> it's user's first time here.") attrs = params_from_ueberauth(auth) case Users.register_user(attrs) do {:ok, user} -> Logger.debug("user=#{inspect(user)}") conn |> put_flash(:success, "Welcome to Futureporn!") |> sign_in_and_redirect(user, ~p"/profile") {:error, reason} -> Logger.error("failed Users.register_user for a first-time user. reason=#{reason}") conn |> put_flash(:error, "Something went wrong. Please try again.") |> redirect(to: ~p"/") end end end def callback( conn = %{assigns: %{ueberauth_failure: %Ueberauth.Failure{errors: errors}}}, _params ) do error_messages = errors |> Enum.map(fn %Ueberauth.Failure.Error{message: message} -> message end) # Join multiple errors into a single string if needed |> Enum.join(", ") conn |> put_flash(:error, "Authentication failed: #{error_messages}") |> redirect(to: ~p"/") end defp params_from_ueberauth(%{provider: :github, info: info, uid: uid}) do %{name: info.name, handle: info.nickname, github_handle: info.nickname, github_id: uid} end defp params_from_ueberauth(%{ provider: :patreon, info: info, uid: uid, extra: %{raw_info: %{user: user}} }) do Logger.debug( "getting params_from_ueberauth provider: :patreon. info=#{inspect(info)}, uid=#{inspect(uid)}, user=#{inspect(user)}" ) currently_entitled_tiers = Bright.Patrons.PatreonEntitlements.extract_tiers(user) Logger.debug("currently_entitled_tiers=#{inspect(currently_entitled_tiers)}") patron_tier = TierMapper.largest_tier_for_platform("patreon", currently_entitled_tiers) Logger.debug(">>>> computed patron_tier=#{inspect(patron_tier)}") %{ patreon_id: uid, patron_tier: patron_tier } end defp sign_in_and_redirect(conn, user, route) do Logger.debug("sign_in_and_redirect with user=#{inspect(user)} route=#{route}") user |> User.sign_in_changes() |> Repo.update() conn # |> tap(fn c -> Logger.debug("Before: #{inspect(c.assigns)}") end) |> assign(:current_user, user) # |> tap(fn c -> Logger.debug("After: #{inspect(c.assigns)}") end) |> put_flash(:success, "Authenticated! teehee.") |> put_session("id", user.id) |> configure_session(renew: true) |> redirect(to: route) end defp maybe_store_return_to(%{method: "GET"} = conn) do put_session(conn, :user_return_to, current_path(conn)) end defp maybe_store_return_to(conn), do: conn @doc """ Authenticates the user by looking into the session. """ def fetch_current_user(conn, _opts) do user_id = get_session(conn, :id) user = user_id && Users.get_user!(user_id) Logger.debug( "fetch_current_user attempting to get user. user_id=#{inspect(user_id)} user=#{inspect(user)}" ) assign(conn, :current_user, user) end def on_mount(:current_user, _params, session, socket) do Logger.debug("~~~~ on_mount with :current_user=#{inspect(:current_user)}") case session do %{"id" => id} -> {:cont, Phoenix.Component.assign_new(socket, :current_user, fn -> Users.get_user!(id) end)} {:cont, assign(socket, :page_title, "DemoWeb")} %{} -> {:cont, Phoenix.Component.assign(socket, :current_user, nil)} end end def on_mount(:ensure_authenticated, _params, session, socket) do Logger.debug("on_mount with session=#{inspect(session)}") case session do %{"id" => id} -> Logger.debug( "~~~~ on_mount with :ensure_authenticated=#{inspect(:ensure_authenticated)}. user id=#{id}" ) new_socket = Phoenix.Component.assign_new(socket, :current_user, fn -> Users.get_user!(id) end) Logger.debug("new_socket=#{inspect(new_socket)}") %Users.User{} = new_socket.assigns.current_user {:cont, new_socket} %{} -> Logger.debug("~~~~ on_mount with ensure_authenticated... user id was FALSY! (bad)") {:halt, redirect_require_login(socket)} end rescue Ecto.NoResultsError -> {:halt, redirect_require_login(socket)} end def on_mount(:ensure_admin, _params, session, socket) do Logger.debug("~~~~ on_mount with :ensure_admin=#{inspect(:ensure_admin)}") case session do %{"id" => id} -> user = Users.get_user!(id) if Users.admin?(user) do {:cont, socket} else {:halt, LiveView.redirect(socket, to: ~p"/")} end %{} -> {:halt, redirect_require_login(socket)} end rescue Ecto.NoResultsError -> {:halt, redirect_require_login(socket)} end @doc """ Used for routes that require the user to be authenticated. """ def require_authenticated_user(conn, _opts) do if conn.assigns[:current_user] do conn else conn |> put_flash(:error, "You must log in to access this page.") |> maybe_store_return_to() |> redirect(to: ~p"/auth/patreon") |> halt() end end def require_patron_tier_1(conn, _), do: require_patron_tier(conn, 1) def require_patron_tier_2(conn, _), do: require_patron_tier(conn, 2) def require_patron_tier_3(conn, _), do: require_patron_tier(conn, 3) @doc """ Ensures the user has a patron tier of `tier_level` or higher, or has the role of "admin". Redirects users who don't meet the requirement. """ def require_patron_tier(conn, tier_level) when is_integer(tier_level) do user = conn.assigns[:current_user] || %{} Logger.debug( "require_patron_tier with conn=#{inspect(conn)} and tier_level=#{inspect(tier_level)} and user=#{inspect(user)}" ) case user do %{role: "admin"} -> conn %{patron_tier: patron_tier} when is_integer(patron_tier) and patron_tier >= tier_level -> conn _ -> user_return_to = conn.assigns[:user_return_to] || "/" conn |> put_flash(:error, "This route is for tier #{tier_level} or higher.") |> redirect(to: user_return_to) |> halt() end end @doc """ Used for routes that require the user to have admin privs. """ def require_admin_user(conn, _opts) do case conn.assigns[:current_user] do %{role: "admin"} -> conn user -> Logger.debug( "A USER IS DOING A THING THAT REQUIRES ADMIN USER. BUT WE THINK THEY ARE NOT AN ADMIN. user=#{inspect(user)}" ) conn |> put_flash(:error, "Only admins can do that.") |> maybe_store_return_to() |> redirect(to: ~p"/") |> halt() end end defp redirect_require_login(socket) do socket |> LiveView.put_flash(:error, "Please sign in") |> LiveView.redirect(to: ~p"/auth/patreon") end end