Features: - User registration and authentication with email/password - Admin login with username-based authentication (separate from regular users) - Review system for contractors to rate clients - Star rating system with review forms - Client identification with private data protection - Contractor registration with document verification - Admin dashboard for review management - Contact form (demo, non-functional) - Responsive navigation with DaisyUI components - Docker Compose setup for production deployment - PostgreSQL database with Ecto migrations - High Vis color scheme (dark background with safety orange/green) Admin credentials: username: admin, password: admin123 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
173 lines
4.3 KiB
Elixir
173 lines
4.3 KiB
Elixir
defmodule MyFirstElixirVibeCodeWeb.UserAuth do
|
|
@moduledoc """
|
|
User authentication module for managing user sessions.
|
|
"""
|
|
|
|
use MyFirstElixirVibeCodeWeb, :verified_routes
|
|
|
|
import Plug.Conn
|
|
import Phoenix.Controller
|
|
|
|
alias MyFirstElixirVibeCode.Accounts
|
|
|
|
@max_age 60 * 60 * 24 * 60 # 60 days
|
|
@remember_me_cookie "_my_first_elixir_vibe_code_web_user_remember_me"
|
|
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
|
|
|
|
# Plug behaviour
|
|
def init(opts), do: opts
|
|
|
|
def call(conn, opts) when is_atom(opts) do
|
|
apply(__MODULE__, opts, [conn, opts])
|
|
end
|
|
|
|
@doc """
|
|
Logs the user in.
|
|
|
|
It renews the session ID to avoid fixation attacks.
|
|
"""
|
|
def log_in_user(conn, user) do
|
|
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
|
|
user_return_to = get_session(conn, :user_return_to)
|
|
|
|
conn
|
|
|> renew_session()
|
|
|> put_token_in_session(token)
|
|
|> put_session(:user_id, user.id)
|
|
|> put_session(:is_admin, user.is_admin)
|
|
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|
|
|> redirect(to: user_return_to || signed_in_path(user))
|
|
end
|
|
|
|
defp signed_in_path(user) do
|
|
if user.is_admin do
|
|
~p"/admin/dashboard"
|
|
else
|
|
~p"/"
|
|
end
|
|
end
|
|
|
|
defp renew_session(conn) do
|
|
conn
|
|
|> configure_session(renew: true)
|
|
|> clear_session()
|
|
end
|
|
|
|
@doc """
|
|
Logs the user out.
|
|
|
|
It clears all session data for safety.
|
|
"""
|
|
def log_out_user(conn) do
|
|
if live_socket_id = get_session(conn, :live_socket_id) do
|
|
MyFirstElixirVibeCodeWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
|
end
|
|
|
|
conn
|
|
|> renew_session()
|
|
|> delete_resp_cookie(@remember_me_cookie)
|
|
|> redirect(to: ~p"/")
|
|
end
|
|
|
|
@doc """
|
|
Authenticates the user by looking into the session.
|
|
"""
|
|
def fetch_current_user(conn, _opts) do
|
|
user_id = get_session(conn, :user_id)
|
|
user = user_id && Accounts.get_user!(user_id)
|
|
assign(conn, :current_user, user)
|
|
end
|
|
|
|
@doc """
|
|
Handles mounting and authenticating the current_user in LiveViews.
|
|
"""
|
|
def on_mount(:mount_current_user, _params, session, socket) do
|
|
{:cont, mount_current_user(socket, session)}
|
|
end
|
|
|
|
def on_mount(:ensure_authenticated, _params, session, socket) do
|
|
socket = mount_current_user(socket, session)
|
|
|
|
if socket.assigns.current_user do
|
|
{:cont, socket}
|
|
else
|
|
socket =
|
|
socket
|
|
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|
|
|> Phoenix.LiveView.redirect(to: ~p"/login")
|
|
|
|
{:halt, socket}
|
|
end
|
|
end
|
|
|
|
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
|
|
socket = mount_current_user(socket, session)
|
|
|
|
if socket.assigns.current_user do
|
|
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket.assigns.current_user))}
|
|
else
|
|
{:cont, socket}
|
|
end
|
|
end
|
|
|
|
defp mount_current_user(socket, session) do
|
|
Phoenix.Component.assign_new(socket, :current_user, fn ->
|
|
if user_id = session["user_id"] do
|
|
Accounts.get_user!(user_id)
|
|
end
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Used for routes that require the user to not be authenticated.
|
|
"""
|
|
def redirect_if_user_is_authenticated(conn, _opts) do
|
|
if conn.assigns[:current_user] do
|
|
conn
|
|
|> redirect(to: signed_in_path(conn.assigns.current_user))
|
|
|> halt()
|
|
else
|
|
conn
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Used for routes that require the user to be authenticated.
|
|
"""
|
|
def require_authenticated_user(conn, _opts) do
|
|
if conn.assigns[:current_user] do
|
|
conn
|
|
else
|
|
conn
|
|
|> put_flash(:error, "You must log in to access this page.")
|
|
|> maybe_store_return_to()
|
|
|> redirect(to: ~p"/login")
|
|
|> halt()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Used for routes that require the user to be an admin.
|
|
"""
|
|
def require_admin_user(conn, _opts) do
|
|
if conn.assigns[:current_user] && conn.assigns.current_user.is_admin do
|
|
conn
|
|
else
|
|
conn
|
|
|> put_flash(:error, "You must be an admin to access this page.")
|
|
|> redirect(to: ~p"/")
|
|
|> halt()
|
|
end
|
|
end
|
|
|
|
defp put_token_in_session(conn, token) do
|
|
conn
|
|
|> put_session(:user_token, token)
|
|
end
|
|
|
|
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
|
put_session(conn, :user_return_to, current_path(conn))
|
|
end
|
|
|
|
defp maybe_store_return_to(conn), do: conn
|
|
end
|