diff --git a/lib/bike_brigade/authentication_messenger.ex b/lib/bike_brigade/authentication_messenger.ex index 1666315a..16814974 100644 --- a/lib/bike_brigade/authentication_messenger.ex +++ b/lib/bike_brigade/authentication_messenger.ex @@ -29,6 +29,20 @@ defmodule BikeBrigade.AuthenticationMessenger do GenServer.call(pid, {:validate_token, phone, token_attempt}) end + def clear_token(phone), do: clear_token(@name, phone) + + def clear_token(pid, phone) do + GenServer.cast(pid, {:clear, phone}) + end + + if Mix.env() == :test do + def get_state(), do: get_state(@name) + + def get_state(pid) do + GenServer.call(pid, :get_state) + end + end + # Server (callbacks) @impl GenServer @@ -53,7 +67,7 @@ defmodule BikeBrigade.AuthenticationMessenger do def handle_call({:validate_token, phone, token_attempt}, _from, state) do # TODO refactor - if !dev?() do + if dev?() do unless Map.has_key?(state, phone) do {:reply, {:error, :token_expired}, state} else @@ -68,16 +82,29 @@ defmodule BikeBrigade.AuthenticationMessenger do end end + if Mix.env() == :test do + @impl GenServer + def handle_call(:get_state, _from, state) do + {:reply, state, state} + end + end + @impl GenServer def handle_info({:expire, phone}, state) do {:noreply, Map.delete(state, phone)} end + @impl GenServer + def handle_cast({:clear, phone}, state) do + {:noreply, Map.delete(state, phone)} + end + defp send_message(phone, token) do msg = [ from: Messaging.outbound_number(), to: phone, - body: "Your BikeBrigade access code is #{token}.\n\n@#{BikeBrigadeWeb.Endpoint.host()} ##{token}" + body: + "Your BikeBrigade access code is #{token}.\n\n@#{BikeBrigadeWeb.Endpoint.host()} ##{token}" ] SmsService.send_sms(msg) diff --git a/lib/bike_brigade_web/controllers/authentication.ex b/lib/bike_brigade_web/controllers/authentication_controller.ex similarity index 53% rename from lib/bike_brigade_web/controllers/authentication.ex rename to lib/bike_brigade_web/controllers/authentication_controller.ex index ffb149db..2d2980b8 100644 --- a/lib/bike_brigade_web/controllers/authentication.ex +++ b/lib/bike_brigade_web/controllers/authentication_controller.ex @@ -1,4 +1,4 @@ -defmodule BikeBrigadeWeb.Authentication do +defmodule BikeBrigadeWeb.AuthenticationController do use BikeBrigadeWeb, :controller import Plug.Conn @@ -6,7 +6,59 @@ defmodule BikeBrigadeWeb.Authentication do alias BikeBrigade.Accounts alias BikeBrigade.AuthenticationMessenger - def login(conn, %{"login" => %{"phone" => phone, "token_attempt" => token_attempt}}) do + defmodule Login do + use BikeBrigade.Schema + import Ecto.Changeset + + alias BikeBrigade.EctoPhoneNumber + + @primary_key false + embedded_schema do + field :phone, EctoPhoneNumber.Canadian + field :token_attempt, :string + end + + def validate_phone(attrs) do + %Login{} + |> cast(attrs, [:phone]) + |> validate_required([:phone]) + |> validate_user_exists(:phone) + |> Ecto.Changeset.apply_action(:insert) + end + + defp validate_user_exists(changeset, field) when is_atom(field) do + validate_change(changeset, field, fn _, phone -> + case Accounts.get_user_by_phone(phone) do + nil -> [{field, "We can't find your number. Have you signed up for Bike Brigade?"}] + _ -> [] + end + end) + end + end + + def show(conn, %{"login" => attrs}) do + case Login.validate_phone(attrs) do + {:ok, login} -> + changeset = Ecto.Changeset.change(login) + + conn + |> render("show.html", state: :token, changeset: changeset, layout: false) + + {:error, %Ecto.Changeset{} = changeset} -> + conn + |> render("show.html", state: :phone, changeset: changeset, layout: false) + end + end + + def show(conn, _params) do + changeset = Ecto.Changeset.change(%Login{}) + + conn + |> render("show.html", state: :phone, changeset: changeset, layout: false) + end + + def login(conn, %{"login" => %{"phone" => phone, "token_attempt" => token_attempt}}) + when not is_nil(token_attempt) do case AuthenticationMessenger.validate_token(phone, token_attempt) do :ok -> user = Accounts.get_user_by_phone(phone) @@ -24,10 +76,36 @@ defmodule BikeBrigadeWeb.Authentication do {:error, :token_invalid} -> conn |> put_flash(:error, "Access code is invalid. Please try again.") - |> redirect(to: ~p"/login?#{%{phone: phone}}") + |> redirect(to: ~p"/login?#{%{login: %{phone: phone}}}") end end + def login(conn, %{"login" => %{"phone" => phone}}) do + with {:ok, login} <- Login.validate_phone(%{"phone" => phone}), + :ok <- AuthenticationMessenger.generate_token(login.phone) do + changeset = Ecto.Changeset.change(login) + + conn + |> render("show.html", state: :token, changeset: changeset, layout: false) + else + {:error, %Ecto.Changeset{} = changeset} -> + conn + |> render("show.html", state: :phone, changeset: changeset, layout: false) + + {:error, err} -> + conn + |> put_flash(:error, err) + |> redirect(to: ~p"/login") + end + end + + def cancel(conn, %{"phone" => phone}) do + AuthenticationMessenger.clear_token(phone) + + conn + |> redirect(to: ~p"/login") + end + @doc "Set the session token and live socket for the user" def do_login(conn, user) do conn diff --git a/lib/bike_brigade_web/controllers/authentication_html.ex b/lib/bike_brigade_web/controllers/authentication_html.ex new file mode 100644 index 00000000..c2dc716c --- /dev/null +++ b/lib/bike_brigade_web/controllers/authentication_html.ex @@ -0,0 +1,6 @@ +defmodule BikeBrigadeWeb.AuthenticationHTML do + use BikeBrigadeWeb, :html + + embed_templates "authentication_html/*" + +end diff --git a/lib/bike_brigade_web/live/login_live.html.heex b/lib/bike_brigade_web/controllers/authentication_html/show.html.heex similarity index 86% rename from lib/bike_brigade_web/live/login_live.html.heex rename to lib/bike_brigade_web/controllers/authentication_html/show.html.heex index 7cc45c6f..ab0dde06 100644 --- a/lib/bike_brigade_web/live/login_live.html.heex +++ b/lib/bike_brigade_web/controllers/authentication_html/show.html.heex @@ -1,3 +1,7 @@ +<.flash kind={:info} title="Success!" flash={@flash} /> +<.flash kind={:warn} title="Alert!" flash={@flash} /> +<.flash kind={:error} title="Error!" flash={@flash} /> +