Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
138 lines
4.6 KiB
Elixir
138 lines
4.6 KiB
Elixir
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
|