changed project/app name

This commit is contained in:
Adam Piontek 2021-02-24 07:49:39 -05:00
commit cd31432f88
89 changed files with 421 additions and 417 deletions

View file

@ -0,0 +1,35 @@
defmodule Bones73kWeb.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", Bones73kWeb.RoomChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# Bones73kWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
@impl true
def id(_socket), do: nil
end

View file

@ -0,0 +1,158 @@
defmodule Bones73kWeb.UserAuth do
import Plug.Conn
import Phoenix.Controller
alias Bones73k.Accounts
alias Bones73kWeb.Router.Helpers, as: Routes
@pubsub_topic "user_updates"
# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
# the token expiry itself in UserToken.
@max_age 60 * 60 * 24 * 60
@remember_me_cookie "user_remember_me"
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
@doc """
Logs the user in.
It renews the session ID and clears the whole session
to avoid fixation attacks. See the renew_session
function to customize this behaviour.
It also sets a `:live_socket_id` key in the session,
so LiveView sessions are identified and automatically
disconnected on log out. The line can be safely removed
if you are not using LiveView.
"""
def log_in_user(conn, user, params \\ %{}) do
token = Accounts.generate_user_session_token(user)
user_return_to = get_session(conn, :user_return_to)
conn
|> renew_session()
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|> maybe_write_remember_me_cookie(token, params)
|> redirect(to: user_return_to || signed_in_path(conn))
end
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end
defp maybe_write_remember_me_cookie(conn, _token, _params) do
conn
end
# This function renews the session ID and erases the whole
# session to avoid fixation attacks. If there is any data
# in the session you may want to preserve after log in/log out,
# you must explicitly fetch the session data before clearing
# and then immediately set it after clearing, for example:
#
# defp renew_session(conn) do
# preferred_locale = get_session(conn, :preferred_locale)
#
# conn
# |> configure_session(renew: true)
# |> clear_session()
# |> put_session(:preferred_locale, preferred_locale)
# 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. See renew_session.
"""
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do
Bones73kWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: "/")
end
@doc """
Authenticates the user by looking into the session
and remember me token.
"""
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)
end
defp ensure_user_token(conn) do
if user_token = get_session(conn, :user_token) do
{user_token, conn}
else
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
if user_token = conn.cookies[@remember_me_cookie] do
{user_token, put_session(conn, :user_token, user_token)}
else
{nil, conn}
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))
|> halt()
else
conn
end
end
@doc """
Used for routes that require the user to be authenticated.
If you want to enforce the user email is confirmed before
they use the application at all, here would be a good place.
"""
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: Routes.user_session_path(conn, :new))
|> halt()
end
end
@doc """
Returns the pubsub topic name for receiving notifications when a user updated
"""
def pubsub_topic, do: @pubsub_topic
defp maybe_store_return_to(%{method: "GET"} = conn) do
%{request_path: request_path, query_string: query_string} = conn
return_to = if query_string == "", do: request_path, else: request_path <> "?" <> query_string
put_session(conn, :user_return_to, return_to)
end
defp maybe_store_return_to(conn), do: conn
defp signed_in_path(_conn), do: "/"
end

View file

@ -0,0 +1,43 @@
defmodule Bones73kWeb.UserConfirmationController do
use Bones73kWeb, :controller
alias Bones73k.Accounts
def new(conn, _params) do
render(conn, "new.html")
end
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
)
end
# Regardless of the outcome, show an impartial success/error message.
conn
|> put_flash(
:info,
"If your email is in our system and it has not been confirmed yet, " <>
"you will receive an email with instructions shortly."
)
|> redirect(to: "/")
end
# Do not log in the user after confirmation to avoid a
# leaked token giving the user access to the account.
def confirm(conn, %{"token" => token}) do
case Accounts.confirm_user(token) do
{:ok, _} ->
conn
|> put_flash(:info, "Account confirmed successfully.")
|> redirect(to: "/")
:error ->
conn
|> put_flash(:error, "Confirmation link is invalid or it has expired.")
|> redirect(to: "/")
end
end
end

View file

@ -0,0 +1,30 @@
defmodule Bones73kWeb.UserRegistrationController do
use Bones73kWeb, :controller
alias Bones73k.Accounts
alias Bones73k.Accounts.User
alias Bones73kWeb.UserAuth
def new(conn, _params) do
changeset = Accounts.change_user_registration(%User{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_params}) do
case Accounts.register_user(user_params) do
{:ok, user} ->
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
)
conn
|> put_flash(:info, "User created successfully.")
|> UserAuth.log_in_user(user)
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
end

View file

@ -0,0 +1,59 @@
defmodule Bones73kWeb.UserResetPasswordController do
use Bones73kWeb, :controller
alias Bones73k.Accounts
plug(:get_user_by_reset_password_token when action in [:edit, :update])
def new(conn, _params) do
render(conn, "new.html")
end
def create(conn, %{"user" => %{"email" => email}}) do
if user = Accounts.get_user_by_email(email) do
Accounts.deliver_user_reset_password_instructions(
user,
&Routes.user_reset_password_url(conn, :edit, &1)
)
end
# Regardless of the outcome, show an impartial success/error message.
conn
|> put_flash(
:info,
"If your email is in our system, you will receive instructions to reset your password shortly."
)
|> redirect(to: "/")
end
def edit(conn, _params) do
render(conn, "edit.html", changeset: Accounts.change_user_password(conn.assigns.user))
end
# Do not log in the user after reset password to avoid a
# leaked token giving the user access to the account.
def update(conn, %{"user" => user_params}) do
case Accounts.reset_user_password(conn.assigns.user, user_params) do
{:ok, _} ->
conn
|> put_flash(:info, "Password reset successfully.")
|> redirect(to: Routes.user_session_path(conn, :new))
{:error, changeset} ->
render(conn, "edit.html", changeset: changeset)
end
end
defp get_user_by_reset_password_token(conn, _opts) do
%{"token" => token} = conn.params
if user = Accounts.get_user_by_reset_password_token(token) do
conn |> assign(:user, user) |> assign(:token, token)
else
conn
|> put_flash(:error, "Reset password link is invalid or it has expired.")
|> redirect(to: "/")
|> halt()
end
end
end

View file

@ -0,0 +1,35 @@
defmodule Bones73kWeb.UserSessionController do
use Bones73kWeb, :controller
alias Bones73k.Accounts
alias Bones73kWeb.UserAuth
def new(conn, _params) do
render(conn, "new.html", error_message: nil)
end
def create(conn, %{"user" => user_params}) do
%{"email" => email, "password" => password} = user_params
if user = Accounts.get_user_by_email_and_password(email, password) do
UserAuth.log_in_user(conn, user, user_params)
else
render(conn, "new.html", error_message: "Invalid email or password")
end
end
def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> UserAuth.log_out_user()
end
def force_logout(conn, _params) do
conn
|> put_flash(
:info,
"You were logged out. Please login again to continue using our application."
)
|> UserAuth.log_out_user()
end
end

View file

@ -0,0 +1,72 @@
defmodule Bones73kWeb.UserSettingsController do
use Bones73kWeb, :controller
alias Bones73k.Accounts
alias Bones73kWeb.UserAuth
plug(:assign_email_and_password_changesets)
def edit(conn, _params) do
render(conn, "edit.html")
end
def update_email(conn, %{"current_password" => password, "user" => user_params}) do
user = conn.assigns.current_user
case Accounts.apply_user_email(user, password, user_params) do
{:ok, applied_user} ->
Accounts.deliver_update_email_instructions(
applied_user,
user.email,
&Routes.user_settings_url(conn, :confirm_email, &1)
)
conn
|> put_flash(
:info,
"A link to confirm your email change has been sent to the new address."
)
|> redirect(to: Routes.user_settings_path(conn, :edit))
{:error, changeset} ->
render(conn, "edit.html", email_changeset: changeset)
end
end
def confirm_email(conn, %{"token" => token}) do
case Accounts.update_user_email(conn.assigns.current_user, token) do
:ok ->
conn
|> put_flash(:info, "Email changed successfully.")
|> redirect(to: Routes.user_settings_path(conn, :edit))
:error ->
conn
|> put_flash(:error, "Email change link is invalid or it has expired.")
|> redirect(to: Routes.user_settings_path(conn, :edit))
end
end
def update_password(conn, %{"current_password" => password, "user" => user_params}) do
user = conn.assigns.current_user
case Accounts.update_user_password(user, password, user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "Password updated successfully.")
|> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
|> UserAuth.log_in_user(user)
{:error, changeset} ->
render(conn, "edit.html", password_changeset: changeset)
end
end
defp assign_email_and_password_changesets(conn, _opts) do
user = conn.assigns.current_user
conn
|> assign(:email_changeset, Accounts.change_user_email(user))
|> assign(:password_changeset, Accounts.change_user_password(user))
end
end

View file

@ -0,0 +1,58 @@
defmodule Bones73kWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :bones73k
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_bones73k_key",
signing_salt: "9CKxo0VJ"
]
socket("/socket", Bones73kWeb.UserSocket,
websocket: true,
longpoll: false
)
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug(Plug.Static,
at: "/",
from: :bones73k,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
plug(Phoenix.LiveReloader)
plug(Phoenix.CodeReloader)
plug(Phoenix.Ecto.CheckRepoStatus, otp_app: :bones73k)
end
plug(Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
)
plug(Plug.RequestId)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
plug(Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
)
plug(Plug.MethodOverride)
plug(Plug.Head)
plug(Plug.Session, @session_options)
plug(Bones73kWeb.Router)
end

View file

@ -0,0 +1,24 @@
defmodule Bones73kWeb.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
import Bones73kWeb.Gettext
# Simple translation
gettext("Here is the string to translate")
# Plural translation
ngettext("Here is the string to translate",
"Here are the strings to translate",
3)
# Domain-based translation
dgettext("errors", "Here is the error message to translate")
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :bones73k
end

View file

@ -0,0 +1,18 @@
defmodule Bones73kWeb.AdminDashboardLive do
use Bones73kWeb, :live_view
@impl true
def mount(_params, session, socket) do
socket = assign_defaults(session, socket)
{:ok, socket}
end
@impl true
def render(assigns) do
~L"""
<section class="phx-hero">
<h1>Welcome to the admin dashboard!</h1>
</section>
"""
end
end

View file

@ -0,0 +1,53 @@
defmodule Bones73kWeb.LiveHelpers do
import Phoenix.LiveView
alias Bones73k.Accounts
alias Bones73k.Accounts.User
alias Bones73kWeb.Router.Helpers, as: Routes
alias Bones73kWeb.UserAuth
import Phoenix.LiveView.Helpers
@doc """
Renders a component inside the `Bones73kWeb.ModalComponent` component.
The rendered modal receives a `:return_to` option to properly update
the URL when the modal is closed.
## Examples
<%= live_modal @socket, Bones73kWeb.PropertyLive.FormComponent,
id: @property.id || :new,
action: @live_action,
property: @property,
return_to: Routes.property_index_path(@socket, :index) %>
"""
def live_modal(socket, component, opts) do
path = Keyword.fetch!(opts, :return_to)
modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
live_component(socket, Bones73kWeb.ModalComponent, modal_opts)
end
def assign_defaults(session, socket) do
Bones73kWeb.Endpoint.subscribe(UserAuth.pubsub_topic())
socket =
assign_new(socket, :current_user, fn ->
find_current_user(session)
end)
case socket.assigns.current_user do
%User{} ->
socket
_other ->
socket
|> put_flash(:error, "You must log in to access this page.")
|> redirect(to: Routes.user_session_path(socket, :new))
end
end
defp find_current_user(session) do
with user_token when not is_nil(user_token) <- session["user_token"],
%User{} = user <- Accounts.get_user_by_session_token(user_token),
do: user
end
end

View file

@ -0,0 +1,26 @@
defmodule Bones73kWeb.ModalComponent do
use Bones73kWeb, :live_component
@impl true
def render(assigns) do
~L"""
<div id="<%= @id %>" class="phx-modal"
phx-capture-click="close"
phx-window-keydown="close"
phx-key="escape"
phx-target="#<%= @id %>"
phx-page-loading>
<div class="phx-modal-content">
<%= live_patch raw("&times;"), to: @return_to, class: "phx-modal-close" %>
<%= live_component @socket, @component, @opts %>
</div>
</div>
"""
end
@impl true
def handle_event("close", _, socket) do
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
end
end

View file

@ -0,0 +1,40 @@
defmodule Bones73kWeb.PageLive do
use Bones73kWeb, :live_view
@impl true
def mount(_params, session, socket) do
socket = assign_defaults(session, socket)
{:ok, assign(socket, query: "", results: %{})}
end
@impl true
def handle_event("suggest", %{"q" => query}, socket) do
{:noreply, assign(socket, results: search(query), query: query)}
end
@impl true
def handle_event("search", %{"q" => query}, socket) do
case search(query) do
%{^query => vsn} ->
{:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
_ ->
{:noreply,
socket
|> put_flash(:error, "No dependencies found matching \"#{query}\"")
|> assign(results: %{}, query: query)}
end
end
defp search(query) do
if not Bones73kWeb.Endpoint.config(:code_reloader) do
raise "action disabled when not in development"
end
for {app, desc, vsn} <- Application.started_applications(),
app = to_string(app),
String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
into: %{},
do: {app, vsn}
end
end

View file

@ -0,0 +1,48 @@
<section class="phx-hero">
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
<p>Peace of mind from prototype to production</p>
<form phx-change="suggest" phx-submit="search">
<input type="text" name="q" value="<%= @query %>" placeholder="Live dependency search" list="results" autocomplete="off"/>
<datalist id="results">
<%= for {app, _vsn} <- @results do %>
<option value="<%= app %>"><%= app %></option>
<% end %>
</datalist>
<button type="submit" phx-disable-with="Searching...">Go to Hexdocs</button>
</form>
</section>
<section class="row">
<article class="column">
<h2>Resources</h2>
<ul>
<li>
<a href="https://hexdocs.pm/phoenix/overview.html">Guides &amp; Docs</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix">Source</a>
</li>
<li>
<a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a>
</li>
</ul>
</article>
<article class="column">
<h2>Help</h2>
<ul>
<li>
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
</li>
<li>
<a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
</li>
<li>
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
</li>
<li>
<a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
</li>
</ul>
</article>
</section>

View file

@ -0,0 +1,58 @@
defmodule Bones73kWeb.PropertyLive.FormComponent do
use Bones73kWeb, :live_component
alias Bones73k.Properties
@impl true
def update(%{property: property} = assigns, socket) do
changeset = Properties.change_property(property)
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)}
end
@impl true
def handle_event("validate", %{"property" => property_params}, socket) do
changeset =
socket.assigns.property
|> Properties.change_property(property_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
def handle_event("save", %{"property" => property_params}, socket) do
save_property(socket, socket.assigns.action, property_params)
end
defp save_property(socket, :edit, property_params) do
case Properties.update_property(socket.assigns.property, property_params) do
{:ok, _property} ->
{:noreply,
socket
|> put_flash(:info, "Property updated successfully")
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
defp save_property(socket, :new, property_params) do
current_user = socket.assigns.current_user
property_params = Map.put(property_params, "user_id", current_user.id)
case Properties.create_property(property_params) do
{:ok, _property} ->
{:noreply,
socket
|> put_flash(:info, "Property created successfully")
|> push_redirect(to: socket.assigns.return_to)}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
end

View file

@ -0,0 +1,22 @@
<h2><%= @title %></h2>
<%= f = form_for @changeset, "#",
id: "property-form",
phx_target: @myself,
phx_change: "validate",
phx_submit: "save" %>
<%= label f, :name %>
<%= text_input f, :name %>
<%= error_tag f, :name %>
<%= label f, :price %>
<%= number_input f, :price, step: "any" %>
<%= error_tag f, :price %>
<%= label f, :description %>
<%= textarea f, :description %>
<%= error_tag f, :description %>
<%= submit "Save", phx_disable_with: "Saving..." %>
</form>

View file

@ -0,0 +1,77 @@
defmodule Bones73kWeb.PropertyLive.Index do
use Bones73kWeb, :live_view
alias Bones73k.Properties
alias Bones73k.Properties.Property
alias Bones73kWeb.Roles
@impl true
def mount(_params, session, socket) do
socket = assign_defaults(session, socket)
{:ok, assign(socket, :properties, [])}
end
@impl true
def handle_params(params, _url, socket) do
current_user = socket.assigns.current_user
live_action = socket.assigns.live_action
property = property_from_params(params)
if Roles.can?(current_user, property, live_action) do
socket = assign(socket, :properties, list_properties())
{:noreply, apply_action(socket, live_action, params)}
else
{:noreply,
socket
|> put_flash(:error, "Unauthorised")
|> redirect(to: "/")}
end
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Property")
|> assign(:property, Properties.get_property!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Property")
|> assign(:property, %Property{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Properties")
|> assign(:property, nil)
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
current_user = socket.assigns.current_user
property = Properties.get_property!(id)
if Bones73kWeb.Roles.can?(current_user, property, :delete) do
property = Properties.get_property!(id)
{:ok, _} = Properties.delete_property(property)
{:noreply, assign(socket, :properties, list_properties())}
else
{:noreply,
socket
|> put_flash(:error, "Unauthorised")
|> redirect(to: "/")}
end
end
defp property_from_params(params)
defp property_from_params(%{"id" => id}),
do: Properties.get_property!(id)
defp property_from_params(_params), do: %Property{}
defp list_properties do
Properties.list_properties()
end
end

View file

@ -0,0 +1,46 @@
<h1>Listing Properties</h1>
<%= if @live_action in [:new, :edit] do %>
<%= live_modal @socket, Bones73kWeb.PropertyLive.FormComponent,
id: @property.id || :new,
title: @page_title,
action: @live_action,
property: @property,
current_user: @current_user,
return_to: Routes.property_index_path(@socket, :index) %>
<% end %>
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody id="properties">
<%= for property <- @properties do %>
<tr id="property-<%= property.id %>">
<td><%= property.name %></td>
<td><%= property.price %></td>
<td><%= property.description %></td>
<td>
<%= if Roles.can?(@current_user, property, :show) do %>
<span><%= live_redirect "Show", to: Routes.property_show_path(@socket, :show, property) %></span>
<% end %>
<%= if Roles.can?(@current_user, property, :edit) do %>
<span><%= live_patch "Edit", to: Routes.property_index_path(@socket, :edit, property) %></span>
<% end %>
<%= if Roles.can?(@current_user, property, :delete) do %>
<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: property.id, data: [confirm: "Are you sure?"] %></span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<span><%= live_patch "New Property", to: Routes.property_index_path(@socket, :new) %></span>

View file

@ -0,0 +1,34 @@
defmodule Bones73kWeb.PropertyLive.Show do
use Bones73kWeb, :live_view
alias Bones73k.Properties
alias Bones73kWeb.Roles
@impl true
def mount(_params, session, socket) do
socket = assign_defaults(session, socket)
{:ok, socket}
end
@impl true
def handle_params(%{"id" => id}, _, socket) do
current_user = socket.assigns.current_user
live_action = socket.assigns.live_action
property = Properties.get_property!(id)
if Roles.can?(current_user, property, live_action) do
{:noreply,
socket
|> assign(:property, property)
|> assign(:page_title, page_title(live_action))}
else
{:noreply,
socket
|> put_flash(:error, "Unauthorised")
|> redirect(to: "/")}
end
end
defp page_title(:show), do: "Show Property"
defp page_title(:edit), do: "Edit Property"
end

View file

@ -0,0 +1,36 @@
<h1>Show Property</h1>
<%= if @live_action in [:edit] do %>
<%= live_modal @socket, Bones73kWeb.PropertyLive.FormComponent,
id: @property.id,
title: @page_title,
action: @live_action,
property: @property,
return_to: Routes.property_show_path(@socket, :show, @property) %>
<% end %>
<ul>
<li>
<strong>Name:</strong>
<%= @property.name %>
</li>
<li>
<strong>Price:</strong>
<%= @property.price %>
</li>
<li>
<strong>Description:</strong>
<%= @property.description %>
</li>
</ul>
<%= if Roles.can?(@current_user, @property, :edit) do %>
<span><%= live_patch "Edit", to: Routes.property_show_path(@socket, :edit, @property), class: "button" %></span>
<% end %>
<%= if Roles.can?(@current_user, @property, :index) do %>
<span><%= live_redirect "Back", to: Routes.property_index_path(@socket, :index) %></span>
<% end %>

View file

@ -0,0 +1,18 @@
defmodule Bones73kWeb.UserDashboardLive do
use Bones73kWeb, :live_view
@impl true
def mount(_params, session, socket) do
socket = assign_defaults(session, socket)
{:ok, socket}
end
@impl true
def render(assigns) do
~L"""
<section class="phx-hero">
<h1>Welcome to the user dashboard!</h1>
</section>
"""
end
end

View file

@ -0,0 +1,52 @@
defmodule Bones73kWeb.EnsureRolePlug do
@moduledoc """
This plug ensures that a user has a particular role before accessing a given route.
## Example
Let's suppose we have three roles: :admin, :manager and :user.
If you want a user to have at least manager role, so admins and managers are authorised to access a given route
plug Bones73kWeb.EnsureRolePlug, [:admin, :manager]
If you want to give access only to an admin:
plug Bones73kWeb.EnsureRolePlug, :admin
"""
import Plug.Conn
alias Bones73k.Accounts
alias Bones73k.Accounts.User
alias Phoenix.Controller
alias Plug.Conn
@doc false
@spec init(any()) :: any()
def init(config), do: config
@doc false
@spec call(Conn.t(), atom() | [atom()]) :: Conn.t()
def call(conn, roles) do
user_token = get_session(conn, :user_token)
(user_token &&
Accounts.get_user_by_session_token(user_token))
|> has_role?(roles)
|> maybe_halt(conn)
end
defp has_role?(%User{} = user, roles) when is_list(roles),
do: Enum.any?(roles, &has_role?(user, &1))
defp has_role?(%User{role: role}, role), do: true
defp has_role?(_user, _role), do: false
defp maybe_halt(true, conn), do: conn
defp maybe_halt(_any, conn) do
conn
|> Controller.put_flash(:error, "Unauthorised")
|> Controller.redirect(to: signed_in_path(conn))
|> halt()
end
defp signed_in_path(_conn), do: "/"
end

21
lib/bones73k_web/roles.ex Normal file
View file

@ -0,0 +1,21 @@
defmodule Bones73kWeb.Roles do
@moduledoc """
Defines roles related functions.
"""
alias Bones73k.Accounts.User
alias Bones73k.Properties.Property
@type entity :: struct()
@type action :: :new | :index | :edit | :show | :delete
@spec can?(%User{}, entity(), action()) :: boolean()
def can?(user, entity, action)
def can?(%User{role: :admin}, %Property{}, _any), do: true
def can?(%User{}, %Property{}, :index), do: true
def can?(%User{}, %Property{}, :new), do: true
def can?(%User{}, %Property{}, :show), do: true
def can?(%User{id: id}, %Property{user_id: id}, :edit), do: true
def can?(%User{id: id}, %Property{user_id: id}, :delete), do: true
def can?(_, _, _), do: false
end

103
lib/bones73k_web/router.ex Normal file
View file

@ -0,0 +1,103 @@
defmodule Bones73kWeb.Router do
use Bones73kWeb, :router
import Bones73kWeb.UserAuth
alias Bones73kWeb.EnsureRolePlug
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, {Bones73kWeb.LayoutView, :root})
plug(:protect_from_forgery)
plug(:put_secure_browser_headers)
plug(:fetch_current_user)
end
pipeline :api do
plug(:accepts, ["json"])
end
pipeline :user do
plug(EnsureRolePlug, [:admin, :user])
end
pipeline :admin do
plug(EnsureRolePlug, :admin)
end
# Other scopes may use custom stacks.
# scope "/api", Bones73kWeb do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through(:browser)
live_dashboard("/dashboard", metrics: Bones73kWeb.Telemetry)
end
end
## Authentication routes
scope "/", Bones73kWeb do
pipe_through([:browser, :redirect_if_user_is_authenticated])
get("/users/register", UserRegistrationController, :new)
post("/users/register", UserRegistrationController, :create)
get("/users/log_in", UserSessionController, :new)
post("/users/log_in", UserSessionController, :create)
get("/users/reset_password", UserResetPasswordController, :new)
post("/users/reset_password", UserResetPasswordController, :create)
get("/users/reset_password/:token", UserResetPasswordController, :edit)
put("/users/reset_password/:token", UserResetPasswordController, :update)
end
scope "/", Bones73kWeb do
pipe_through([:browser, :require_authenticated_user])
get("/users/settings", UserSettingsController, :edit)
put("/users/settings/update_password", UserSettingsController, :update_password)
put("/users/settings/update_email", UserSettingsController, :update_email)
get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email)
# This line was moved
live("/", PageLive, :index)
end
scope "/", Bones73kWeb do
pipe_through([:browser])
get("/users/force_logout", UserSessionController, :force_logout)
delete("/users/log_out", UserSessionController, :delete)
get("/users/confirm", UserConfirmationController, :new)
post("/users/confirm", UserConfirmationController, :create)
get("/users/confirm/:token", UserConfirmationController, :confirm)
end
scope "/", Bones73kWeb do
pipe_through([:browser, :require_authenticated_user, :user])
live("/user_dashboard", UserDashboardLive, :index)
live("/properties", PropertyLive.Index, :index)
live("/properties/new", PropertyLive.Index, :new)
live("/properties/:id/edit", PropertyLive.Index, :edit)
live("/properties/:id", PropertyLive.Show, :show)
live("/properties/:id/show/edit", PropertyLive.Show, :edit)
end
scope "/", Bones73kWeb do
pipe_through([:browser, :require_authenticated_user, :admin])
live("/admin_dashboard", AdminDashboardLive, :index)
end
end

View file

@ -0,0 +1,55 @@
defmodule Bones73kWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
# Telemetry poller will execute the given period measurements
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
# Database Metrics
summary("bones73k.repo.query.total_time", unit: {:native, :millisecond}),
summary("bones73k.repo.query.decode_time", unit: {:native, :millisecond}),
summary("bones73k.repo.query.query_time", unit: {:native, :millisecond}),
summary("bones73k.repo.query.queue_time", unit: {:native, :millisecond}),
summary("bones73k.repo.query.idle_time", unit: {:native, :millisecond}),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {Bones73kWeb, :count_users, []}
]
end
end

View file

@ -0,0 +1,10 @@
<ul>
<%= if @current_user do %>
<li><%= @current_user.email %></li>
<li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
<li><%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
<% else %>
<li><%= link "Register", to: Routes.user_registration_path(@conn, :new) %></li>
<li><%= link "Log in", to: Routes.user_session_path(@conn, :new) %></li>
<% end %>
</ul>

View file

@ -0,0 +1,5 @@
<main role="main" class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<%= @inner_content %>
</main>

View file

@ -0,0 +1,11 @@
<main role="main" class="container">
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
<%= @inner_content %>
</main>

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "Bones73k", suffix: " · Phoenix Framework" %>
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head>
<body>
<header>
<section class="container">
<nav role="navigation">
<ul>
<li><%= link "Properties", to: Routes.property_index_path(@conn, :index) %></li>
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
<li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
<% end %>
</ul>
<%= render "_user_menu.html", assigns %>
</nav>
<a href="https://phoenixframework.org/" class="phx-logo">
<img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
</a>
</section>
</header>
<%= @inner_content %>
</body>
</html>

View file

@ -0,0 +1,15 @@
<h1>Resend confirmation instructions</h1>
<%= form_for :user, Routes.user_confirmation_path(@conn, :create), fn f -> %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<div>
<%= submit "Resend confirmation instructions" %>
</div>
<% end %>
<p>
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
</p>

View file

@ -0,0 +1,26 @@
<h1>Register</h1>
<%= form_for @changeset, Routes.user_registration_path(@conn, :create), fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<%= error_tag f, :email %>
<%= label f, :password %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :password %>
<div>
<%= submit "Register" %>
</div>
<% end %>
<p>
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %> |
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
</p>

View file

@ -0,0 +1,26 @@
<h1>Reset password</h1>
<%= form_for @changeset, Routes.user_reset_password_path(@conn, :update, @token), fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :password, "New password" %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :password %>
<%= label f, :password_confirmation, "Confirm new password" %>
<%= password_input f, :password_confirmation, required: true %>
<%= error_tag f, :password_confirmation %>
<div>
<%= submit "Reset password" %>
</div>
<% end %>
<p>
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
</p>

View file

@ -0,0 +1,15 @@
<h1>Forgot your password?</h1>
<%= form_for :user, Routes.user_reset_password_path(@conn, :create), fn f -> %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<div>
<%= submit "Send instructions to reset password" %>
</div>
<% end %>
<p>
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %>
</p>

View file

@ -0,0 +1,27 @@
<h1>Log in</h1>
<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user], fn f -> %>
<%= if @error_message do %>
<div class="alert alert-danger">
<p><%= @error_message %></p>
</div>
<% end %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<%= label f, :password %>
<%= password_input f, :password, required: true %>
<%= label f, :remember_me, "Keep me logged in for 60 days" %>
<%= checkbox f, :remember_me %>
<div>
<%= submit "Log in" %>
</div>
<% end %>
<p>
<%= link "Register", to: Routes.user_registration_path(@conn, :new) %> |
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
</p>

View file

@ -0,0 +1,49 @@
<h1>Settings</h1>
<h3>Change email</h3>
<%= form_for @email_changeset, Routes.user_settings_path(@conn, :update_email), fn f -> %>
<%= if @email_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :email %>
<%= email_input f, :email, required: true %>
<%= error_tag f, :email %>
<%= label f, :current_password, for: "current_password_for_email" %>
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email" %>
<%= error_tag f, :current_password %>
<div>
<%= submit "Change email" %>
</div>
<% end %>
<h3>Change password</h3>
<%= form_for @password_changeset, Routes.user_settings_path(@conn, :update_password), fn f -> %>
<%= if @password_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :password, "New password" %>
<%= password_input f, :password, required: true %>
<%= error_tag f, :password %>
<%= label f, :password_confirmation, "Confirm new password" %>
<%= password_input f, :password_confirmation, required: true %>
<%= error_tag f, :password_confirmation %>
<%= label f, :current_password, for: "current_password_for_password" %>
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password" %>
<%= error_tag f, :current_password %>
<div>
<%= submit "Change password" %>
</div>
<% end %>

View file

@ -0,0 +1,47 @@
defmodule Bones73kWeb.ErrorHelpers do
@moduledoc """
Conveniences for translating and building error messages.
"""
use Phoenix.HTML
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error),
class: "invalid-feedback",
phx_feedback_for: input_id(form, field)
)
end)
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate "is invalid" in the "errors" domain
# dgettext("errors", "is invalid")
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# Because the error messages we show in our forms and APIs
# are defined inside Ecto, we need to translate them dynamically.
# This requires us to call the Gettext module passing our gettext
# backend as first argument.
#
# Note we use the "errors" domain, which means translations
# should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do
Gettext.dngettext(Bones73kWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(Bones73kWeb.Gettext, "errors", msg, opts)
end
end
end

View file

@ -0,0 +1,16 @@
defmodule Bones73kWeb.ErrorView do
use Bones73kWeb, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.
# def render("500.html", _assigns) do
# "Internal Server Error"
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.html" becomes
# "Not Found".
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

View file

@ -0,0 +1,3 @@
defmodule Bones73kWeb.LayoutView do
use Bones73kWeb, :view
end

View file

@ -0,0 +1,3 @@
defmodule Bones73kWeb.UserConfirmationView do
use Bones73kWeb, :view
end

View file

@ -0,0 +1,3 @@
defmodule Bones73kWeb.UserRegistrationView do
use Bones73kWeb, :view
end

View file

@ -0,0 +1,3 @@
defmodule Bones73kWeb.UserResetPasswordView do
use Bones73kWeb, :view
end

View file

@ -0,0 +1,3 @@
defmodule Bones73kWeb.UserSessionView do
use Bones73kWeb, :view
end

View file

@ -0,0 +1,3 @@
defmodule Bones73kWeb.UserSettingsView do
use Bones73kWeb, :view
end