defmodule TarakanWeb.AccountSessionController do use TarakanWeb, :controller alias Tarakan.Accounts alias TarakanWeb.AccountAuth alias TarakanWeb.BrowserRateLimit def create(conn, %{"_action" => "confirmed"} = params) do rate_limited_create(conn, params, "Account confirmed successfully.") end def create(conn, params) do rate_limited_create(conn, params, "Welcome back!") end defp rate_limited_create(conn, params, info) do account_params = Map.get(params, "account", %{}) identifier = account_params["identifier"] || account_params["email"] || account_params["token"] || account_params["session_token"] || "missing" remote_ip = remote_ip(conn) identifier_key = :crypto.hash(:sha256, identifier |> to_string() |> String.downcase()) checks = [ BrowserRateLimit.allowed?(:login_ip, remote_ip), BrowserRateLimit.allowed?(:login_pair, {remote_ip, identifier_key}) ] if Enum.all?(checks) do create(conn, params, info) else conn |> put_flash(:error, "Invalid credentials or too many attempts. Try again later.") |> redirect(to: ~p"/accounts/log-in") end end # instant registration login (one-time session bootstrap token; does not # confirm the email address the way a magic-link login does) defp create(conn, %{"account" => %{"session_token" => encoded} = account_params}, info) do with {:ok, token} <- Base.url_decode64(encoded, padding: false), {account, _inserted_at} <- Accounts.get_account_by_session_token(token) do # One-time bootstrap: log_in_account mints the real session token. Accounts.delete_account_session_token(token) conn |> put_flash(:info, info) |> AccountAuth.log_in_account(account, account_params) else _other -> conn |> put_flash(:error, "The link is invalid or it has expired.") |> redirect(to: ~p"/accounts/log-in") end end # magic link login defp create(conn, %{"account" => %{"token" => token} = account_params}, info) do case Accounts.login_account_by_magic_link(token) do {:ok, {account, disconnect_ref}} -> AccountAuth.disconnect_sessions(disconnect_ref) conn |> put_flash(:info, info) |> AccountAuth.log_in_account(account, account_params) _ -> conn |> put_flash(:error, "The link is invalid or it has expired.") |> redirect(to: ~p"/accounts/log-in") end end # handle/email + password login defp create(conn, %{"account" => account_params}, info) do identifier = Map.get(account_params, "identifier", "") password = Map.get(account_params, "password", "") if account = Accounts.get_account_by_identifier_and_password(identifier, password) do conn |> put_flash(:info, info) |> AccountAuth.log_in_account(account, account_params) else # Do not disclose whether the handle or email is registered. conn |> put_flash(:error, "Invalid handle, email, or password") |> put_flash(:identifier, String.slice(identifier, 0, 160)) |> redirect(to: ~p"/accounts/log-in") end end def update_password(conn, %{"account" => account_params}) do account = conn.assigns.current_scope.account cond do not Accounts.sudo_mode?(account) -> # The sudo window can expire between rendering the form and POSTing it; # send the account through re-authentication instead of crashing. conn |> put_flash( :error, "Confirm it's you with a magic link before changing sensitive settings." ) |> redirect(to: AccountAuth.reauth_path(~p"/accounts/settings")) is_nil(account.confirmed_at) -> conn |> put_flash( :error, "Confirm your email address with a login link before setting a password." ) |> redirect(to: ~p"/accounts/settings") true -> {:ok, {account, disconnect_ref}} = Accounts.update_account_password(account, account_params) # disconnect all existing LiveViews with old sessions AccountAuth.disconnect_sessions(disconnect_ref) conn |> put_session(:account_return_to, ~p"/accounts/settings") |> put_flash(:info, "Password updated successfully!") |> AccountAuth.log_in_account(account) end end def delete(conn, _params) do conn |> put_flash(:info, "Logged out successfully.") |> AccountAuth.log_out_account() end # Bucket by /64 for IPv6 so prefix rotation cannot mint fresh rate-limit buckets. defp remote_ip(conn), do: TarakanWeb.Plugs.ClientIp.remote_ip_bucket(conn) end