Janky initial implementation of auth and profiles

This commit is contained in:
Ryan Pandya 2022-10-29 17:21:52 -07:00
parent 2284a437fa
commit 90007d40e7
12 changed files with 232 additions and 76 deletions

View File

@ -2,6 +2,8 @@ defmodule Friends.Accounts.User do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
@repo Friends.Repo
schema "users" do schema "users" do
field :email, :string field :email, :string
field :password, :string, virtual: true, redact: true field :password, :string, virtual: true, redact: true
@ -13,6 +15,15 @@ defmodule Friends.Accounts.User do
has_one :profile, Friends.Friend has_one :profile, Friends.Friend
end 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 """ @doc """
A user changeset for registration. A user changeset for registration.

View File

@ -35,20 +35,26 @@ defmodule Friends.Friend do
def changeset(friend, params \\ %{}) do def changeset(friend, params \\ %{}) do
friend 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_required([:name, :email, :phone, :born])
|> Ecto.Changeset.validate_format(:name, ~r/\w+\ \w+/) |> 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(
|> Ecto.Changeset.validate_format(:phone, Regex.compile!("^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$")) :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(:name)
|> Ecto.Changeset.unique_constraint(:email)
end end
def all() do def all() do
preloads = [:relationships, :reverse_relationships] preloads = [:relationships, :reverse_relationships, :user]
@repo.all(from(f in Friends.Friend, preload: ^preloads)) @repo.all(from(f in Friends.Friend, preload: ^preloads))
end end
def new(params \\ %{}) do def new(params \\ %{}) do
%Friend{id: "new"} %Friend{id: "new"}
|> struct(params) |> struct(params)
@ -62,6 +68,7 @@ defmodule Friends.Friend do
) )
) )
end end
def get_by_id(id) do def get_by_id(id) do
@repo.one( @repo.one(
from(f in Friend, from(f in Friend,
@ -71,49 +78,61 @@ defmodule Friends.Friend do
) )
end 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 def create_or_update(params) do
case params.id do case params.id do
"new" -> "new" ->
%Friend{} |> Friend.changeset(%{params | id: nil}) %Friend{}
|> Map.put(:action, :insert) |> Friend.changeset(%{params | id: nil})
|> @repo.insert! |> Map.put(:action, :insert)
_number -> |> @repo.insert!
Friend.get_by_id(params.id |> String.to_integer)
|> Friend.changeset(params) _number ->
|> Map.put(:action, :update) Friend.get_by_id(params.id |> String.to_integer())
|> @repo.update! |> Friend.changeset(params)
|> Map.put(:action, :update)
|> @repo.update!
end end
end end
def get_relationships(friend) do def get_relationships(friend) do
friend friend
|> relations |> relations
|> Enum.map(&(relation(friend, &1))) |> Enum.map(&relation(friend, &1))
end end
def get_events(friend) do def get_events(friend) do
friend friend
|> get_relationships |> get_relationships
|> Enum.map(&(&1.events)) |> Enum.map(& &1.events)
|> List.flatten |> List.flatten()
end end
def age(friend) do def age(friend) do
# Age in years # Age in years
Date.diff(Date.utc_today,friend.born) |> div(365) Date.diff(Date.utc_today(), friend.born) |> div(365)
end end
# TODO: Refactor # TODO: Refactor
def create_birth_event(friend) do def create_birth_event(friend) do
if is_nil @repo.all( if is_nil(
from(e in Friends.Event, @repo.all(
where: e.name == "born" from(e in Friends.Event,
)) |> List.flatten do where: e.name == "born"
)
)
|> List.flatten()
) do
else else
"find" "find"
end end
end end
end end

View File

@ -1,16 +1,63 @@
defmodule FriendsWeb.PageController do defmodule FriendsWeb.PageController do
use FriendsWeb, :controller use FriendsWeb, :controller
alias Friends.{Friend, Relationship} alias Friends.{Friend, Repo, Accounts.User}
import Helpers
import Helpers.Names
plug :assign_profile
def index(conn, _params) do def index(conn, _params) do
new_friend = Friend.new() |> Friend.changeset()
new_friend = Friend.new |> Friend.changeset
conn conn
|> assign(:new_friend, new_friend) |> assign(:new_friend, new_friend)
|> assign(:all_friends, Friend.all) |> assign(:all_friends, Friend.all())
|> assign(:users, Friends.Accounts.User |> Friends.Repo.all) |> assign(:users, User |> Repo.all())
|> render("index.html") |> render("index.html")
end 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 end

View File

@ -91,7 +91,13 @@ defmodule FriendsWeb.UserAuth do
def fetch_current_user(conn, _opts) do def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn) {user_token, conn} = ensure_user_token(conn)
user = user_token && Accounts.get_user_by_session_token(user_token) 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 end
defp ensure_user_token(conn) do defp ensure_user_token(conn) do

View File

@ -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

View File

@ -23,8 +23,8 @@ defmodule FriendsWeb.Router do
get "/", PageController, :index get "/", PageController, :index
get "/friends", FriendController, :index get "/friends", FriendController, :index
live "/friend/:slug", FriendsLive.Show live "/friend/:slug", FriendLive.Show
live "/friend/:slug/edit", FriendLive.Edit
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.
@ -65,6 +65,7 @@ defmodule FriendsWeb.Router do
scope "/", FriendsWeb do scope "/", FriendsWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated] pipe_through [:browser, :redirect_if_user_is_authenticated]
# Requires the user NOT be authenticated:
get "/users/register", UserRegistrationController, :new get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create post "/users/register", UserRegistrationController, :create
@ -78,6 +79,9 @@ defmodule FriendsWeb.Router do
scope "/", FriendsWeb do scope "/", FriendsWeb do
pipe_through [:browser, :require_authenticated_user] pipe_through [:browser, :require_authenticated_user]
# Requires the user DO be authenticated:
live "/profile/new", ProfileLive.Form, :new
get "/users/settings", UserSettingsController, :edit get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update put "/users/settings", UserSettingsController, :update

View File

@ -3,27 +3,8 @@
<div class="flex-1"> <div class="flex-1">
<.link navigate={"/"} class="btn btn-ghost normal-case text-xl">Friends</.link> <.link navigate={"/"} class="btn btn-ghost normal-case text-xl">Friends</.link>
</div> </div>
<div class="flex-none gap-2"> <div class="dropdown dropdown-end">
<div class="form-control hidden md:block"> <%= render("_user_menu.html", conn: @socket, current_user: @current_user) %>
<input type="text" placeholder="Search" class="input input-bordered" />
</div>
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<img src="https://placeimg.com/80/80/people" />
</div>
</label>
<ul tabindex="0" class="mt-3 p-2 shadow menu menu-compact dropdown-content bg-base-100 rounded-box w-52">
<li>
<a>
Profile
</a>
</li>
<li class="md:hidden"><a>Search</a></li>
<li><a>Settings</a></li>
<li><a>Logout</a></li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -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"""
<b>Your email:</b> <%= @current_user.email %>
"""
end
def show_form(%{step: 2} = assigns) do
~H"""
Step 2
"""
end
end

View File

@ -0,0 +1,9 @@
<div class="prose">
<h1>Welcome!</h1>
<p>You have made a user account, but we don't yet have a profile for you.</p>
<div id="profile-form">
<.show_form step={1} current_user={@current_user} />
</div>
</div>

View File

@ -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

View File

@ -0,0 +1,3 @@
defmodule UserProfileView do
end

View File

@ -1,6 +1,4 @@
defmodule Helpers do defmodule Helpers do
import Helpers.Names
def do!(thing) do def do!(thing) do
case thing do case thing do
{:ok, answer} -> answer {:ok, answer} -> answer
@ -8,21 +6,21 @@ defmodule Helpers do
end end
end end
def pluralize(qty, word) do def pluralize(qty, word) do
plural = if qty == 1 do plural =
"#{word}" if qty == 1 do
else "#{word}"
"#{word}s" else
end "#{word}s"
end
"#{qty} #{plural}" "#{qty} #{plural}"
end end
def parse_date(str), do: str |> to_string |> Timex.parse("%Y-%m-%d", :strftime) def parse_date(str), do: str |> to_string |> Timex.parse("%Y-%m-%d", :strftime)
def format_phone(str) do def format_phone(str) do
str str |> String.replace(" ", "") |> String.replace("-", "") |> String.replace(".", "")
|> String.replace(" ", "") |> String.replace("-", "") |> String.replace(".", "")
end end
def to_slug(name) when is_binary(name) do def to_slug(name) when is_binary(name) do
@ -30,6 +28,7 @@ defmodule Helpers do
|> String.replace(" ", "-") |> String.replace(" ", "-")
|> String.downcase() |> String.downcase()
end end
def to_slug(obj), do: to_slug(obj.name) def to_slug(obj), do: to_slug(obj.name)
def from_slug(name) do def from_slug(name) do
@ -41,17 +40,20 @@ defmodule Helpers do
end end
def birthday(friend) do def birthday(friend) do
this_year = (Date.utc_today this_year =
|> Calendar.strftime("%Y") Date.utc_today()
|> String.to_integer) |> Calendar.strftime("%Y")
|> String.to_integer()
next_birthday = friend.born next_birthday =
|> Calendar.strftime("%m-%d") friend.born
|> Calendar.strftime("%m-%d")
next_birthdate = "#{this_year}-#{next_birthday}" next_birthdate =
|> Date.from_iso8601!() "#{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!() "#{this_year + 1}-#{next_birthday}" |> Date.from_iso8601!()
else else
next_birthdate next_birthdate
@ -59,7 +61,7 @@ defmodule Helpers do
end end
def time_until_birthday(friend) do def time_until_birthday(friend) do
birthday(friend) |> Date.diff(Date.utc_today) birthday(friend) |> Date.diff(Date.utc_today())
end end
def relations(friend) do def relations(friend) do
@ -74,5 +76,4 @@ defmodule Helpers do
def events(relationship) do def events(relationship) do
relationship.events relationship.events
end end
end end