162 lines
4.9 KiB
Elixir
162 lines
4.9 KiB
Elixir
defmodule MvWeb.LinkOidcAccountLive do
|
|
@moduledoc """
|
|
LiveView for linking an OIDC account to an existing password account.
|
|
|
|
This page is shown when a user tries to log in via OIDC using an email
|
|
that already exists with a password-only account. The user must verify
|
|
their password before the OIDC account can be linked.
|
|
"""
|
|
use MvWeb, :live_view
|
|
require Ash.Query
|
|
|
|
@impl true
|
|
def mount(_params, session, socket) do
|
|
user_id = Map.get(session, "oidc_linking_user_id")
|
|
oidc_user_info = Map.get(session, "oidc_linking_user_info")
|
|
|
|
if user_id && oidc_user_info do
|
|
# Load the user
|
|
case Ash.get(Mv.Accounts.User, user_id) do
|
|
{:ok, user} ->
|
|
{:ok,
|
|
socket
|
|
|> assign(:user, user)
|
|
|> assign(:oidc_user_info, oidc_user_info)
|
|
|> assign(:password, "")
|
|
|> assign(:error, nil)
|
|
|> assign(:form, to_form(%{"password" => ""}))}
|
|
|
|
{:error, _} ->
|
|
{:ok,
|
|
socket
|
|
|> put_flash(:error, gettext("Session expired. Please try again."))
|
|
|> redirect(to: ~p"/sign-in")}
|
|
end
|
|
else
|
|
{:ok,
|
|
socket
|
|
|> put_flash(:error, gettext("Invalid session. Please try again."))
|
|
|> redirect(to: ~p"/sign-in")}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"password" => password}, socket) do
|
|
{:noreply, assign(socket, :password, password)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("submit", %{"password" => password}, socket) do
|
|
user = socket.assigns.user
|
|
oidc_user_info = socket.assigns.oidc_user_info
|
|
|
|
# Verify the password using AshAuthentication
|
|
case verify_password(user.email, password) do
|
|
{:ok, verified_user} ->
|
|
# Password correct - link the OIDC account
|
|
link_oidc_account(socket, verified_user, oidc_user_info)
|
|
|
|
{:error, _reason} ->
|
|
# Password incorrect
|
|
{:noreply,
|
|
socket
|
|
|> assign(:error, gettext("Incorrect password. Please try again."))
|
|
|> assign(:form, to_form(%{"password" => ""}))}
|
|
end
|
|
end
|
|
|
|
defp verify_password(email, password) do
|
|
# Use AshAuthentication password strategy to verify
|
|
strategies = AshAuthentication.Info.authentication_strategies(Mv.Accounts.User)
|
|
password_strategy = Enum.find(strategies, fn s -> s.name == :password end)
|
|
|
|
if password_strategy do
|
|
AshAuthentication.Strategy.Password.Actions.sign_in(
|
|
password_strategy,
|
|
%{
|
|
"email" => email,
|
|
"password" => password
|
|
},
|
|
[]
|
|
)
|
|
else
|
|
{:error, "Password authentication not configured"}
|
|
end
|
|
end
|
|
|
|
defp link_oidc_account(socket, user, oidc_user_info) do
|
|
oidc_id = Map.get(oidc_user_info, "sub") || Map.get(oidc_user_info, "id")
|
|
|
|
# Update the user with the OIDC ID
|
|
case Mv.Accounts.User
|
|
|> Ash.Query.filter(id == ^user.id)
|
|
|> Ash.read_one!()
|
|
|> Ash.Changeset.for_update(:link_oidc_id, %{
|
|
oidc_id: oidc_id,
|
|
oidc_user_info: oidc_user_info
|
|
})
|
|
|> Ash.update() do
|
|
{:ok, _updated_user} ->
|
|
# After successful linking, redirect to OIDC login
|
|
# Since the user now has an oidc_id, the next OIDC login will succeed
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(
|
|
:info,
|
|
gettext(
|
|
"Your OIDC account has been successfully linked! Redirecting to complete sign-in..."
|
|
)
|
|
)
|
|
|> Phoenix.LiveView.redirect(to: ~p"/auth/user/rauthy")}
|
|
|
|
{:error, error} ->
|
|
{:noreply,
|
|
socket
|
|
|> assign(:error, gettext("Failed to link account: %{error}", error: inspect(error)))
|
|
|> assign(:form, to_form(%{"password" => ""}))}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<div class="mx-auto max-w-sm mt-16">
|
|
<.header class="text-center">
|
|
{gettext("Link OIDC Account")}
|
|
<:subtitle>
|
|
{gettext(
|
|
"An account with email %{email} already exists. Please enter your password to link your OIDC account.",
|
|
email: @user.email
|
|
)}
|
|
</:subtitle>
|
|
</.header>
|
|
|
|
<.form for={@form} id="link-oidc-form" phx-submit="submit" phx-change="validate" class="mt-8">
|
|
<div class="space-y-6">
|
|
<div>
|
|
<.input field={@form[:password]} type="password" label={gettext("Password")} required />
|
|
</div>
|
|
|
|
<%= if @error do %>
|
|
<div class="rounded-md bg-red-50 p-4">
|
|
<p class="text-sm text-red-800">{@error}</p>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div>
|
|
<.button phx-disable-with={gettext("Linking...")} class="w-full">
|
|
{gettext("Link Account")}
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
|
|
<div class="mt-4 text-center text-sm">
|
|
<.link navigate={~p"/sign-in"} class="text-brand hover:underline">
|
|
{gettext("Cancel")}
|
|
</.link>
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
end
|