-34
lib/tasty/accounts.ex
-34
lib/tasty/accounts.ex
···
1
-
defmodule Tasty.Accounts do
2
-
import Ecto.Query, warn: false
3
-
alias Tasty.Repo
4
-
alias Tasty.Accounts.User
5
-
alias Bcrypt, as: Pbkdf
6
-
7
-
def register_user(attrs) do
8
-
%User{}
9
-
|> User.registration_changeset(attrs)
10
-
|> Repo.insert()
11
-
end
12
-
13
-
def authenticate_user(email, password) do
14
-
user = Repo.get_by(User, email: email)
15
-
{:ok, user}
16
-
case Bcrypt.verify_pass(password, user.password_hash) do
17
-
true ->
18
-
{:ok, user}
19
-
false ->
20
-
{:error, :invalid_credentials}
21
-
end
22
-
23
-
# cond do
24
-
# user && Pbkdf.verify_pass(password, user.password_hash) ->
25
-
26
-
# {:ok, user}
27
-
28
-
# true ->
29
-
# {:error, :invalid_credentials}
30
-
# end
31
-
end
32
-
33
-
def get_user(id), do: Repo.get(User, id)
34
-
end
···
-33
lib/tasty/accounts/user.ex
-33
lib/tasty/accounts/user.ex
···
1
-
defmodule Tasty.Accounts.User do
2
-
use Ecto.Schema
3
-
import Ecto.Changeset
4
-
alias Bcrypt, as: Pbkdf
5
-
6
-
schema "users" do
7
-
field :email, :string
8
-
field :password_hash, :string
9
-
10
-
# Virtual fields are not stored in DB but used for password changes
11
-
field :password, :string, virtual: true
12
-
13
-
timestamps()
14
-
end
15
-
16
-
def registration_changeset(user, attrs) do
17
-
user
18
-
|> cast(attrs, [:email, :password])
19
-
|> validate_required([:email, :password])
20
-
|> unique_constraint(:email)
21
-
|> hash_password()
22
-
end
23
-
24
-
defp hash_password(changeset) do
25
-
case changeset do
26
-
%Ecto.Changeset{valid?: true, changes: %{password: plain}} ->
27
-
put_change(changeset, :password_hash, Pbkdf.hash_pwd_salt(plain))
28
-
29
-
_ ->
30
-
changeset
31
-
end
32
-
end
33
-
end
···
-1
lib/tasty_web.ex
-1
lib/tasty_web.ex
+3
-1
lib/tasty_web/controllers/page_controller.ex
+3
-1
lib/tasty_web/controllers/page_controller.ex
+1
-13
lib/tasty_web/controllers/page_html/home.html.heex
+1
-13
lib/tasty_web/controllers/page_html/home.html.heex
···
60
</p>
61
<div class="flex">
62
<div class="w-full sm:w-auto">
63
-
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4">
64
<a
65
href="https://hexdocs.pm/phoenix/overview.html"
66
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
···
215
Deploy your application
216
</a>
217
</div>
218
-
</div>
219
-
<div>
220
-
<h1>Welcome!</h1>
221
-
<p>This is your home page.</p>
222
-
<p>Current user: <%= inspect @current_user %></p>
223
-
<p>
224
-
<.link navigate={~p"/register"}>Register</.link>
225
-
|
226
-
<.link navigate={~p"/log_in"}>Log In</.link>
227
-
|
228
-
<.link navigate={~p"/log_out"}>Log Out</.link>
229
-
</p>
230
</div>
231
</div>
232
</div>
···
60
</p>
61
<div class="flex">
62
<div class="w-full sm:w-auto">
63
+
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-3">
64
<a
65
href="https://hexdocs.pm/phoenix/overview.html"
66
class="group relative rounded-2xl px-6 py-4 text-sm font-semibold leading-6 text-zinc-900 sm:py-6"
···
215
Deploy your application
216
</a>
217
</div>
218
</div>
219
</div>
220
</div>
-52
lib/tasty_web/live/user_login_live.ex
-52
lib/tasty_web/live/user_login_live.ex
···
1
-
defmodule TastyWeb.UserLoginLive do
2
-
use TastyWeb, :live_view
3
-
4
-
alias Tasty.Accounts
5
-
6
-
def mount(_params, _session, socket) do
7
-
form = to_form(%{}, as: :user)
8
-
{:ok, assign(socket, error: nil, form: form)}
9
-
end
10
-
11
-
def handle_event("login", %{"user" => %{"email" => email, "password" => pass}}, socket) do
12
-
case Accounts.authenticate_user(email, pass) do
13
-
{:ok, user} ->
14
-
socket =
15
-
socket
16
-
|> put_flash(:info, "Logged in successfully.")
17
-
|> assign(error: nil)
18
-
|> then(fn s -> Phoenix.LiveView.redirect(s, to: ~p"/") end)
19
-
20
-
{:noreply, store_user_in_session(socket, user.id)}
21
-
22
-
{:error, :invalid_credentials} ->
23
-
{:noreply, assign(socket, error: "Invalid credentials")}
24
-
end
25
-
end
26
-
27
-
defp store_user_in_session(socket, user_id) do
28
-
# We need to store user_id in the session for future requests.
29
-
# Since this is purely LiveView, we can manipulate the session
30
-
# by using the Phoenix.LiveView.redirect/2 and passing a session param,
31
-
# or we can push an event. The simplest approach is to do:
32
-
Phoenix.Component.update(socket, :conn, fn conn ->
33
-
Plug.Conn.put_session(conn, :user_id, user_id)
34
-
end)
35
-
socket
36
-
end
37
-
38
-
def render(assigns) do
39
-
~H"""
40
-
<h2>Log In</h2>
41
-
<%= if @error do %>
42
-
<p style="color:red;"><%= @error %></p>
43
-
<% end %>
44
-
45
-
<.form for={@form} phx-submit="login" :let={f}>
46
-
<.input field={f[:email]} type="email" label="Email" placeholder="Enter Email" />
47
-
<.input field={f[:password]} type="password" label="Password" placeholder="Enter Password" />
48
-
<.button phx-disable-with="Authenticating...">Log In</.button>
49
-
</.form>
50
-
"""
51
-
end
52
-
end
···
-16
lib/tasty_web/live/user_logout_live.ex
-16
lib/tasty_web/live/user_logout_live.ex
···
1
-
defmodule TastyWeb.UserLogoutLive do
2
-
use TastyWeb, :live_view
3
-
4
-
def mount(_params, _session, socket) do
5
-
# Wipe out session
6
-
socket =
7
-
Phoenix.Component.update(socket, :conn, fn conn ->
8
-
conn
9
-
|> Plug.Conn.configure_session(renew: true)
10
-
|> Plug.Conn.clear_session()
11
-
end)
12
-
13
-
socket = put_flash(socket, :info, "Logged out!")
14
-
{:ok, Phoenix.LiveView.redirect(socket, to: ~p"/")}
15
-
end
16
-
end
···
-58
lib/tasty_web/live/user_registration_live.ex
-58
lib/tasty_web/live/user_registration_live.ex
···
1
-
defmodule TastyWeb.UserRegistrationLive do
2
-
use TastyWeb, :live_view
3
-
4
-
alias Tasty.Accounts
5
-
alias Tasty.Accounts.User
6
-
7
-
def mount(_params, _session, socket) do
8
-
changeset = User.registration_changeset(%User{}, %{})
9
-
form = to_form(changeset)
10
-
{:ok, assign(socket, changeset: changeset, form: form, success: false)}
11
-
end
12
-
13
-
def handle_event("save", %{"user" => params}, socket) do
14
-
case Accounts.register_user(params) do
15
-
{:ok, _user} ->
16
-
socket =
17
-
socket
18
-
|> put_flash(:info, "Registered!")
19
-
|> push_navigate(to: ~p"/")
20
-
21
-
{:noreply, socket}
22
-
23
-
{:error, new_changeset} ->
24
-
# Generate a fresh form from your updated changeset
25
-
{:noreply, assign(socket,
26
-
changeset: new_changeset,
27
-
form: to_form(new_changeset)
28
-
)}
29
-
end
30
-
end
31
-
32
-
def render(assigns) do
33
-
~H"""
34
-
<h2>Register</h2>
35
-
<.form for={@form} phx-submit="save" :let={f}>
36
-
<%= if @changeset.action == :insert do %>
37
-
<p style="color:red;">Oops, something went wrong!</p>
38
-
<% end %>
39
-
40
-
<.input
41
-
field={f[:email]}
42
-
type="email"
43
-
label="Email"
44
-
placeholder="Enter Email" />
45
-
46
-
<.input
47
-
field={f[:password]}
48
-
type="password"
49
-
label="Password"
50
-
placeholder="Enter Password" />
51
-
52
-
<.button phx-disable-with="Saving...">
53
-
Register
54
-
</.button>
55
-
</.form>
56
-
"""
57
-
end
58
-
end
···
-11
lib/tasty_web/router.ex
-11
lib/tasty_web/router.ex
···
8
plug :put_root_layout, html: {TastyWeb.Layouts, :root}
9
plug :protect_from_forgery
10
plug :put_secure_browser_headers
11
-
plug :fetch_current_user
12
end
13
14
pipeline :api do
···
17
18
scope "/", TastyWeb do
19
pipe_through :browser
20
-
21
-
live "/register", UserRegistrationLive, :new
22
-
live "/log_in", UserLoginLive, :new
23
-
live "/log_out", UserLogoutLive, :index
24
25
get "/", PageController, :home
26
end
···
45
live_dashboard "/dashboard", metrics: TastyWeb.Telemetry
46
forward "/mailbox", Plug.Swoosh.MailboxPreview
47
end
48
-
end
49
-
50
-
# A minimal plug to load current_user from the session
51
-
defp fetch_current_user(conn, _opts) do
52
-
conn
53
-
|> assign(:current_user, get_session(conn, :user_id) && Tasty.Accounts.get_user(get_session(conn, :user_id)))
54
end
55
end
···
8
plug :put_root_layout, html: {TastyWeb.Layouts, :root}
9
plug :protect_from_forgery
10
plug :put_secure_browser_headers
11
end
12
13
pipeline :api do
···
16
17
scope "/", TastyWeb do
18
pipe_through :browser
19
20
get "/", PageController, :home
21
end
···
40
live_dashboard "/dashboard", metrics: TastyWeb.Telemetry
41
forward "/mailbox", Plug.Swoosh.MailboxPreview
42
end
43
end
44
end
+1
-2
mix.exs
+1
-2
mix.exs
-3
mix.lock
-3
mix.lock
···
1
%{
2
"bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"},
3
-
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"},
4
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
5
-
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
6
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
7
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
8
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
9
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
10
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
11
-
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
12
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
13
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
14
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
···
1
%{
2
"bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"},
3
"castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
4
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
5
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
6
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
7
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
8
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
9
"esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
10
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
11
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
-15
priv/repo/migrations/20231005000000_create_users.exs
-15
priv/repo/migrations/20231005000000_create_users.exs
···
1
-
defmodule Tasty.Repo.Migrations.CreateUsers do
2
-
use Ecto.Migration
3
-
4
-
def change do
5
-
create table(:users) do
6
-
add :email, :string, null: false
7
-
add :password_hash, :string, null: false
8
-
# You can add more fields as needed, e.g., username, bio, etc.
9
-
10
-
timestamps()
11
-
end
12
-
13
-
create unique_index(:users, [:email])
14
-
end
15
-
end
···