From 55c128e25e3ed3b5bb8ae4f11d39fae8b9078c54 Mon Sep 17 00:00:00 2001 From: Bent Witthold Date: Wed, 18 Feb 2026 15:01:31 +0100 Subject: [PATCH] Login per email token. With logout of previous account if already logged in. --- lib/beet_round_server/accounts.ex | 7 ++++++ lib/beet_round_server/accounts/user_token.ex | 21 ++++++++++++++++ .../controllers/user_json.ex | 3 --- .../controllers/user_session_controller.ex | 17 +++++++++++++ lib/beet_round_server_web/router.ex | 2 ++ lib/beet_round_server_web/user_auth.ex | 25 ++++++++++++++++++- 6 files changed, 71 insertions(+), 4 deletions(-) diff --git a/lib/beet_round_server/accounts.ex b/lib/beet_round_server/accounts.ex index fe32145..3e319aa 100644 --- a/lib/beet_round_server/accounts.ex +++ b/lib/beet_round_server/accounts.ex @@ -57,6 +57,13 @@ defmodule BeetRoundServer.Accounts do if User.valid_password?(user, password), do: user end + def get_user_by_email_token(token) do + {:ok, query} = + UserToken.verify_email_token_query(token, "session") + + Repo.one(query) + end + @doc """ Gets a single user. diff --git a/lib/beet_round_server/accounts/user_token.ex b/lib/beet_round_server/accounts/user_token.ex index e468842..727d393 100644 --- a/lib/beet_round_server/accounts/user_token.ex +++ b/lib/beet_round_server/accounts/user_token.ex @@ -125,6 +125,27 @@ defmodule BeetRoundServer.Accounts.UserToken do end end + def verify_email_token_query(token, context) do + case Base.url_decode64(token, padding: false) do + {:ok, decoded_token} -> + hashed_token = :crypto.hash(@hash_algorithm, decoded_token) + days = days_for_context(context) + + query = + from token in by_token_and_context_query(hashed_token, context), + join: user in assoc(token, :user), + where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email, + select: user + + {:ok, query} + + :error -> + :error + end + end + + defp days_for_context("session"), do: @session_validity_in_days + @doc """ Checks if the token is valid and returns its underlying lookup query. diff --git a/lib/beet_round_server_web/controllers/user_json.ex b/lib/beet_round_server_web/controllers/user_json.ex index d178a9f..b0beab8 100644 --- a/lib/beet_round_server_web/controllers/user_json.ex +++ b/lib/beet_round_server_web/controllers/user_json.ex @@ -15,9 +15,6 @@ defmodule BeetRoundServerWeb.UserJSON do %{data: %{email: user.email, id: user.id, token: encoded_token}} end - @doc """ - Renders a single user. - """ def show(%{user: user}) do %{data: data(user)} end diff --git a/lib/beet_round_server_web/controllers/user_session_controller.ex b/lib/beet_round_server_web/controllers/user_session_controller.ex index 19ab0f1..75f5b69 100644 --- a/lib/beet_round_server_web/controllers/user_session_controller.ex +++ b/lib/beet_round_server_web/controllers/user_session_controller.ex @@ -12,6 +12,23 @@ defmodule BeetRoundServerWeb.UserSessionController do create(conn, params, "Welcome back!") end + def login(conn, %{"token" => token}) do + IO.puts("Login via token:") + IO.inspect(token) + + UserAuth.log_out_user_without_redirect(conn) + + if user = Accounts.get_user_by_email_token(token) do + conn + |> put_flash(:info, "Login successful!") + |> UserAuth.log_in_without_creating_cookie(user) + else + conn + |> put_flash(:error, "Invalid token") + |> redirect(to: ~p"/") + end + end + # magic link login defp create(conn, %{"user" => %{"token" => token} = user_params}, info) do case Accounts.login_user_by_magic_link(token) do diff --git a/lib/beet_round_server_web/router.ex b/lib/beet_round_server_web/router.ex index 96de44c..c5f1aa2 100644 --- a/lib/beet_round_server_web/router.ex +++ b/lib/beet_round_server_web/router.ex @@ -88,5 +88,7 @@ defmodule BeetRoundServerWeb.Router do post "/users/log-in", UserSessionController, :create delete "/users/log-out", UserSessionController, :delete + + get "/log_in/:token", UserSessionController, :login end end diff --git a/lib/beet_round_server_web/user_auth.ex b/lib/beet_round_server_web/user_auth.ex index c7b27da..4f38a52 100644 --- a/lib/beet_round_server_web/user_auth.ex +++ b/lib/beet_round_server_web/user_auth.ex @@ -40,6 +40,16 @@ defmodule BeetRoundServerWeb.UserAuth do |> redirect(to: user_return_to || signed_in_path(conn)) end + def log_in_without_creating_cookie(conn, user) do + token = Accounts.generate_user_session_token(user) + user_return_to = get_session(conn, :user_return_to) + + conn + |> renew_session(user) + |> put_token_in_session(token) + |> redirect(to: user_return_to || signed_in_path(conn)) + end + @doc """ Logs the user out. @@ -59,6 +69,19 @@ defmodule BeetRoundServerWeb.UserAuth do |> redirect(to: ~p"/") end + def log_out_user_without_redirect(conn) do + user_token = get_session(conn, :user_token) + user_token && Accounts.delete_user_session_token(user_token) + + if live_socket_id = get_session(conn, :live_socket_id) do + BeetRoundServerWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) + end + + conn + |> renew_session(nil) + |> delete_resp_cookie(@remember_me_cookie) + end + @doc """ Authenticates the user by looking into the session and remember me token. @@ -259,7 +282,7 @@ defmodule BeetRoundServerWeb.UserAuth do @doc "Returns the path to redirect to after log in." # the user was already logged in, redirect to settings def signed_in_path(%Plug.Conn{assigns: %{current_scope: %Scope{user: %Accounts.User{}}}}) do - ~p"/users/settings" + ~p"/biddings" end def signed_in_path(_), do: ~p"/biddings"