From 90007d40e79c1106d4500003ece75377ad0ecf07 Mon Sep 17 00:00:00 2001 From: Ryan Pandya Date: Sat, 29 Oct 2022 17:21:52 -0700 Subject: [PATCH] Janky initial implementation of auth and profiles --- friends/lib/friends/accounts/user.ex | 11 +++ friends/lib/friends/friend.ex | 71 ++++++++++++------- .../controllers/page_controller.ex | 59 +++++++++++++-- .../lib/friends_web/controllers/user_auth.ex | 8 ++- .../controllers/user_profile_controller.ex | 32 +++++++++ friends/lib/friends_web/router.ex | 8 ++- .../templates/layout/live.html.heex | 23 +----- .../templates/profile_live/form.ex | 25 +++++++ .../templates/profile_live/form.html.heex | 9 +++ friends/lib/friends_web/views/live_view.ex | 18 +++++ .../friends_web/views/user_profile_view.ex | 3 + friends/lib/helpers/helpers.ex | 41 +++++------ 12 files changed, 232 insertions(+), 76 deletions(-) create mode 100644 friends/lib/friends_web/controllers/user_profile_controller.ex create mode 100644 friends/lib/friends_web/templates/profile_live/form.ex create mode 100644 friends/lib/friends_web/templates/profile_live/form.html.heex create mode 100644 friends/lib/friends_web/views/live_view.ex create mode 100644 friends/lib/friends_web/views/user_profile_view.ex diff --git a/friends/lib/friends/accounts/user.ex b/friends/lib/friends/accounts/user.ex index 9f85586..db2af5a 100644 --- a/friends/lib/friends/accounts/user.ex +++ b/friends/lib/friends/accounts/user.ex @@ -2,6 +2,8 @@ defmodule Friends.Accounts.User do use Ecto.Schema import Ecto.Changeset + @repo Friends.Repo + schema "users" do field :email, :string field :password, :string, virtual: true, redact: true @@ -13,6 +15,15 @@ defmodule Friends.Accounts.User do has_one :profile, Friends.Friend end + def load_profile(%Friends.Accounts.User{profile: %Ecto.Association.NotLoaded{}} = model) do + model + |> @repo.preload(:profile) + end + + def load_profile(nil) do + %{profile: nil} + end + @doc """ A user changeset for registration. diff --git a/friends/lib/friends/friend.ex b/friends/lib/friends/friend.ex index d2b4f48..27a26f8 100644 --- a/friends/lib/friends/friend.ex +++ b/friends/lib/friends/friend.ex @@ -35,20 +35,26 @@ defmodule Friends.Friend do def changeset(friend, params \\ %{}) do friend - |> Ecto.Changeset.cast(params, [:name, :born, :nickname, :email, :phone, :slug]) + |> Ecto.Changeset.cast(params, [:name, :born, :nickname, :email, :phone, :slug, :user_id]) |> Ecto.Changeset.validate_required([:name, :email, :phone, :born]) |> Ecto.Changeset.validate_format(:name, ~r/\w+\ \w+/) - |> Ecto.Changeset.validate_format(:email, Regex.compile!("^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")) - |> Ecto.Changeset.validate_format(:phone, Regex.compile!("^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$")) + |> Ecto.Changeset.validate_format( + :email, + Regex.compile!("^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$") + ) + |> Ecto.Changeset.validate_format( + :phone, + Regex.compile!("^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$") + ) |> Ecto.Changeset.unique_constraint(:name) + |> Ecto.Changeset.unique_constraint(:email) end def all() do - preloads = [:relationships, :reverse_relationships] + preloads = [:relationships, :reverse_relationships, :user] @repo.all(from(f in Friends.Friend, preload: ^preloads)) end - def new(params \\ %{}) do %Friend{id: "new"} |> struct(params) @@ -62,6 +68,7 @@ defmodule Friends.Friend do ) ) end + def get_by_id(id) do @repo.one( from(f in Friend, @@ -71,49 +78,61 @@ defmodule Friends.Friend do ) end + def get_by_email(email) do + @repo.one( + from(f in Friend, + where: f.email == ^email, + preload: [:relationships, :reverse_relationships] + ) + ) + end + def create_or_update(params) do case params.id do "new" -> - %Friend{} |> Friend.changeset(%{params | id: nil}) - |> Map.put(:action, :insert) - |> @repo.insert! - _number -> - Friend.get_by_id(params.id |> String.to_integer) - |> Friend.changeset(params) - |> Map.put(:action, :update) - |> @repo.update! + %Friend{} + |> Friend.changeset(%{params | id: nil}) + |> Map.put(:action, :insert) + |> @repo.insert! + + _number -> + Friend.get_by_id(params.id |> String.to_integer()) + |> Friend.changeset(params) + |> Map.put(:action, :update) + |> @repo.update! end end def get_relationships(friend) do friend - |> relations - |> Enum.map(&(relation(friend, &1))) + |> relations + |> Enum.map(&relation(friend, &1)) end def get_events(friend) do friend - |> get_relationships - |> Enum.map(&(&1.events)) - |> List.flatten + |> get_relationships + |> Enum.map(& &1.events) + |> List.flatten() end def age(friend) do # Age in years - Date.diff(Date.utc_today,friend.born) |> div(365) + Date.diff(Date.utc_today(), friend.born) |> div(365) end # TODO: Refactor def create_birth_event(friend) do - if is_nil @repo.all( - from(e in Friends.Event, - where: e.name == "born" - )) |> List.flatten do + if is_nil( + @repo.all( + from(e in Friends.Event, + where: e.name == "born" + ) + ) + |> List.flatten() + ) do else "find" end end - - - end diff --git a/friends/lib/friends_web/controllers/page_controller.ex b/friends/lib/friends_web/controllers/page_controller.ex index 7401d19..be9f39e 100644 --- a/friends/lib/friends_web/controllers/page_controller.ex +++ b/friends/lib/friends_web/controllers/page_controller.ex @@ -1,16 +1,63 @@ defmodule FriendsWeb.PageController do use FriendsWeb, :controller - alias Friends.{Friend, Relationship} - import Helpers + alias Friends.{Friend, Repo, Accounts.User} + + import Helpers.Names + + plug :assign_profile def index(conn, _params) do - - new_friend = Friend.new |> Friend.changeset + new_friend = Friend.new() |> Friend.changeset() conn |> assign(:new_friend, new_friend) - |> assign(:all_friends, Friend.all) - |> assign(:users, Friends.Accounts.User |> Friends.Repo.all) + |> assign(:all_friends, Friend.all()) + |> assign(:users, User |> Repo.all()) |> render("index.html") end + + defp assign_profile(conn, _opts) do + current_user = conn.assigns.current_user + + case current_user do + nil -> + # No logged in user, and no profile + conn + + user -> + case user.profile do + nil -> + # Logged in user, but no profile + # Is there a profile with the same email? + try_profile = user.email |> Friends.Friend.get_by_email() + + case try_profile do + nil -> + # If not, we need to make a new profile + conn + |> redirect(to: Routes.profile_form_path(conn, :new)) + + # If so, link 'em and be done with it + friend -> + friend + |> Friends.Friend.changeset(%{ + user_id: user.id + }) + |> Friends.Repo.update!() + + conn + |> put_flash( + :info, + "Welcome to the app, #{friend |> first_name()}!" + ) + |> redirect(to: Routes.friend_path(conn, :index)) + end + + _profile -> + # Logged in user, and linked profile + conn + |> redirect(to: Routes.friend_path(conn, :index)) + end + end + end end diff --git a/friends/lib/friends_web/controllers/user_auth.ex b/friends/lib/friends_web/controllers/user_auth.ex index 30eb900..742a75c 100644 --- a/friends/lib/friends_web/controllers/user_auth.ex +++ b/friends/lib/friends_web/controllers/user_auth.ex @@ -91,7 +91,13 @@ defmodule FriendsWeb.UserAuth do def fetch_current_user(conn, _opts) do {user_token, conn} = ensure_user_token(conn) user = user_token && Accounts.get_user_by_session_token(user_token) - assign(conn, :current_user, user) + + assign( + conn, + :current_user, + user + |> Friends.Repo.preload(:profile) + ) end defp ensure_user_token(conn) do diff --git a/friends/lib/friends_web/controllers/user_profile_controller.ex b/friends/lib/friends_web/controllers/user_profile_controller.ex new file mode 100644 index 0000000..9bd9695 --- /dev/null +++ b/friends/lib/friends_web/controllers/user_profile_controller.ex @@ -0,0 +1,32 @@ +defmodule FriendsWeb.UserProfileController do + use FriendsWeb, :controller + + alias Friends.Accounts + alias FriendsWeb.Router.Helpers, as: Routes + + plug :assign_profile + + + + @doc """ + Checks if the user has linked a profile yet, + and redirects to the profile creation flow + if not. + """ + def main(conn) do + conn + |> put_flash(:info, "test") + end + + defp assign_profile(conn, _opts) do + user = conn.assigns.current_user + + msg = case user.profile do + nil -> "No profile!" + profile -> "Yes profile! #{profile.name}" + end + + conn + |> put_flash(:info, msg) + end +end diff --git a/friends/lib/friends_web/router.ex b/friends/lib/friends_web/router.ex index 5727e62..ef62b00 100644 --- a/friends/lib/friends_web/router.ex +++ b/friends/lib/friends_web/router.ex @@ -23,8 +23,8 @@ defmodule FriendsWeb.Router do get "/", PageController, :index get "/friends", FriendController, :index - live "/friend/:slug", FriendsLive.Show - + live "/friend/:slug", FriendLive.Show + live "/friend/:slug/edit", FriendLive.Edit end # Other scopes may use custom stacks. @@ -65,6 +65,7 @@ defmodule FriendsWeb.Router do scope "/", FriendsWeb do pipe_through [:browser, :redirect_if_user_is_authenticated] + # Requires the user NOT be authenticated: get "/users/register", UserRegistrationController, :new post "/users/register", UserRegistrationController, :create @@ -78,6 +79,9 @@ defmodule FriendsWeb.Router do scope "/", FriendsWeb do pipe_through [:browser, :require_authenticated_user] + # Requires the user DO be authenticated: + + live "/profile/new", ProfileLive.Form, :new get "/users/settings", UserSettingsController, :edit put "/users/settings", UserSettingsController, :update diff --git a/friends/lib/friends_web/templates/layout/live.html.heex b/friends/lib/friends_web/templates/layout/live.html.heex index 9bb672b..432ac4a 100644 --- a/friends/lib/friends_web/templates/layout/live.html.heex +++ b/friends/lib/friends_web/templates/layout/live.html.heex @@ -3,27 +3,8 @@
<.link navigate={"/"} class="btn btn-ghost normal-case text-xl">Friends
-
- - +
diff --git a/friends/lib/friends_web/templates/profile_live/form.ex b/friends/lib/friends_web/templates/profile_live/form.ex new file mode 100644 index 0000000..a30e4d1 --- /dev/null +++ b/friends/lib/friends_web/templates/profile_live/form.ex @@ -0,0 +1,25 @@ +defmodule FriendsWeb.ProfileLive.Form do + use FriendsWeb, :live_view + import FriendsWeb.LiveView + + def mount(%{}, %{"user_token" => token}, socket) do + {:ok, + socket + |> assign_current_user(token) + |> title("Create a profile")} + end + + def show_form(%{step: 1} = assigns) do + # First things first, are we linking to an existing user or no? + + ~H""" + Your email: <%= @current_user.email %> + """ + end + + def show_form(%{step: 2} = assigns) do + ~H""" + Step 2 + """ + end +end diff --git a/friends/lib/friends_web/templates/profile_live/form.html.heex b/friends/lib/friends_web/templates/profile_live/form.html.heex new file mode 100644 index 0000000..f6ad50c --- /dev/null +++ b/friends/lib/friends_web/templates/profile_live/form.html.heex @@ -0,0 +1,9 @@ +
+

Welcome!

+

You have made a user account, but we don't yet have a profile for you.

+ +
+ <.show_form step={1} current_user={@current_user} /> +
+ +
\ No newline at end of file diff --git a/friends/lib/friends_web/views/live_view.ex b/friends/lib/friends_web/views/live_view.ex new file mode 100644 index 0000000..e297ab2 --- /dev/null +++ b/friends/lib/friends_web/views/live_view.ex @@ -0,0 +1,18 @@ +defmodule FriendsWeb.LiveView do + use FriendsWeb, :live_component + + def assign_current_user(socket, user_token) do + socket + |> assign( + :current_user, + user_token + |> Friends.Accounts.get_user_by_session_token() + |> Friends.Repo.preload(:profile) + ) + end + + def title(socket, title) do + socket + |> assign(:page_title, title) + end +end diff --git a/friends/lib/friends_web/views/user_profile_view.ex b/friends/lib/friends_web/views/user_profile_view.ex new file mode 100644 index 0000000..36c9fbe --- /dev/null +++ b/friends/lib/friends_web/views/user_profile_view.ex @@ -0,0 +1,3 @@ +defmodule UserProfileView do + +end diff --git a/friends/lib/helpers/helpers.ex b/friends/lib/helpers/helpers.ex index e637716..982ee96 100644 --- a/friends/lib/helpers/helpers.ex +++ b/friends/lib/helpers/helpers.ex @@ -1,6 +1,4 @@ defmodule Helpers do - import Helpers.Names - def do!(thing) do case thing do {:ok, answer} -> answer @@ -8,21 +6,21 @@ defmodule Helpers do end end - def pluralize(qty, word) do - plural = if qty == 1 do - "#{word}" - else - "#{word}s" - end + plural = + if qty == 1 do + "#{word}" + else + "#{word}s" + end + "#{qty} #{plural}" end def parse_date(str), do: str |> to_string |> Timex.parse("%Y-%m-%d", :strftime) def format_phone(str) do - str - |> String.replace(" ", "") |> String.replace("-", "") |> String.replace(".", "") + str |> String.replace(" ", "") |> String.replace("-", "") |> String.replace(".", "") end def to_slug(name) when is_binary(name) do @@ -30,6 +28,7 @@ defmodule Helpers do |> String.replace(" ", "-") |> String.downcase() end + def to_slug(obj), do: to_slug(obj.name) def from_slug(name) do @@ -41,17 +40,20 @@ defmodule Helpers do end def birthday(friend) do - this_year = (Date.utc_today - |> Calendar.strftime("%Y") - |> String.to_integer) + this_year = + Date.utc_today() + |> Calendar.strftime("%Y") + |> String.to_integer() - next_birthday = friend.born - |> Calendar.strftime("%m-%d") + next_birthday = + friend.born + |> Calendar.strftime("%m-%d") - next_birthdate = "#{this_year}-#{next_birthday}" - |> Date.from_iso8601!() + next_birthdate = + "#{this_year}-#{next_birthday}" + |> Date.from_iso8601!() - if next_birthdate |> Date.diff(Date.utc_today) < 0 do + if next_birthdate |> Date.diff(Date.utc_today()) < 0 do "#{this_year + 1}-#{next_birthday}" |> Date.from_iso8601!() else next_birthdate @@ -59,7 +61,7 @@ defmodule Helpers do end def time_until_birthday(friend) do - birthday(friend) |> Date.diff(Date.utc_today) + birthday(friend) |> Date.diff(Date.utc_today()) end def relations(friend) do @@ -74,5 +76,4 @@ defmodule Helpers do def events(relationship) do relationship.events end - end