initial commit

This commit is contained in:
2021-03-11 21:12:55 -05:00
committed by csculley
commit c954fdc600
89 changed files with 20205 additions and 0 deletions

View File

@ -0,0 +1,35 @@
defmodule CanneryWeb.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", CanneryWeb.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:
#
# CanneryWeb.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,7 @@
defmodule CanneryWeb.PageController do
use CanneryWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end

View File

@ -0,0 +1,149 @@
defmodule CanneryWeb.UserAuth do
import Plug.Conn
import Phoenix.Controller
alias Cannery.Accounts
alias CanneryWeb.Router.Helpers, as: Routes
# 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 "_cannery_web_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
CanneryWeb.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
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
defp signed_in_path(_conn), do: "/"
end

View File

@ -0,0 +1,53 @@
defmodule CanneryWeb.UserConfirmationController do
use CanneryWeb, :controller
alias Cannery.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, "User confirmed successfully.")
|> redirect(to: "/")
:error ->
# If there is a current user and the account was already confirmed,
# then odds are that the confirmation link was already visited, either
# by some automation or by the user themselves, so we redirect without
# a warning message.
case conn.assigns do
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
redirect(conn, to: "/")
%{} ->
conn
|> put_flash(:error, "User confirmation link is invalid or it has expired.")
|> redirect(to: "/")
end
end
end
end

View File

@ -0,0 +1,30 @@
defmodule CanneryWeb.UserRegistrationController do
use CanneryWeb, :controller
alias Cannery.Accounts
alias Cannery.Accounts.User
alias CanneryWeb.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 CanneryWeb.UserResetPasswordController do
use CanneryWeb, :controller
alias Cannery.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,26 @@
defmodule CanneryWeb.UserSessionController do
use CanneryWeb, :controller
alias Cannery.Accounts
alias CanneryWeb.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
end

View File

@ -0,0 +1,74 @@
defmodule CanneryWeb.UserSettingsController do
use CanneryWeb, :controller
alias Cannery.Accounts
alias CanneryWeb.UserAuth
plug :assign_email_and_password_changesets
def edit(conn, _params) do
render(conn, "edit.html")
end
def update(conn, %{"action" => "update_email"} = params) do
%{"current_password" => password, "user" => user_params} = params
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 update(conn, %{"action" => "update_password"} = params) do
%{"current_password" => password, "user" => user_params} = params
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
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
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,54 @@
defmodule CanneryWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :cannery
# 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: "_cannery_key",
signing_salt: "fxAnJltS"
]
socket "/socket", CanneryWeb.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: :cannery,
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: :cannery
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 CanneryWeb.Router
end

View File

@ -0,0 +1,24 @@
defmodule CanneryWeb.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 CanneryWeb.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: :cannery
end

View File

@ -0,0 +1,92 @@
defmodule CanneryWeb.Live.Component.Topbar do
use CanneryWeb, :live_component
alias CanneryWeb.{PageLive}
def mount(socket) do
{:ok, socket |> assign(results: [], title_content: nil)}
end
def update(assigns, socket) do
{:ok, socket |> assign(assigns)}
end
def render(assigns) do
~L"""
<header class="mb-8 px-8 py-4 w-full bg-primary-400">
<nav role="navigation">
<div class="flex flex-row justify-between items-center space-x-4">
<div class="flex flex-row justify-start items-center space-x-2">
<%= link to: Routes.page_path(CanneryWeb.Endpoint, :index) do %>
<h1 class="leading-5 text-xl text-white hover:underline">Cannery</h1>
<% end %>
<%= if @title_content do %>
<span>|</span>
<%= @title_content %>
<% end %>
</div>
<ul class="flex flex-row flex-wrap justify-center items-center
text-lg space-x-4 text-lg text-white">
<%# search %>
<form phx-change="suggest" phx-submit="search">
<input type="text" name="q" class="input input-primary"
placeholder="Search" list="results" autocomplete="off"/>
<datalist id="results">
<%= for {app, _vsn} <- @results do %>
<option value="<%= app %>"><%= app %></option>
<% end %>
</datalist>
</form>
<%# user settings %>
<%= if assigns |> Map.has_key?(:current_user) do %>
<li>
<%= @current_user.email %></li>
<li>
<%= link "Settings", class: "hover:underline",
to: Routes.user_settings_path(CanneryWeb.Endpoint, :edit) %>
</li>
<li>
<%= link "Log out", class: "hover:underline",
to: Routes.user_session_path(CanneryWeb.Endpoint, :delete), method: :delete %>
</li>
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
<li>
<%= link "LiveDashboard", class: "hover:underline",
to: Routes.live_dashboard_path(CanneryWeb.Endpoint, :home) %>
</li>
<% end %>
<% else %>
<li>
<%= link "Register", class: "hover:underline",
to: Routes.user_registration_path(CanneryWeb.Endpoint, :new) %>
</li>
<li>
<%= link "Log in", class: "hover:underline",
to: Routes.user_session_path(CanneryWeb.Endpoint, :new) %>
</li>
<% end %>
</ul>
</div>
</nav>
<%= if live_flash(@flash, :info) do %>
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash" phx-value-key="info">
<%= live_flash(@flash, :info) %>
</p>
<% end %>
<%= if live_flash(@flash, :error) do %>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash" phx-value-key="error">
<%= live_flash(@flash, :error) %>
</p>
<% end %>
</header>
"""
end
end

View File

@ -0,0 +1,39 @@
defmodule CanneryWeb.PageLive do
use CanneryWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{: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 CanneryWeb.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,9 @@
<div class="flex flex-col justify-center items-center text-center space-y-4">
<h1 class="title text-primary-500 text-2xl">
Welcome to Cannery
</h1>
<p class="title text-primary-500 text-lg">
The Self-hosted Ammo Tracker Website
</p>
</div>

73
lib/cannery_web/router.ex Normal file
View File

@ -0,0 +1,73 @@
defmodule CanneryWeb.Router do
use CanneryWeb, :router
import CanneryWeb.UserAuth
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {CanneryWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", CanneryWeb do
pipe_through :browser
live "/", PageLive, :index
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: CanneryWeb.Telemetry
end
end
## Authentication routes
scope "/", CanneryWeb 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 "/", CanneryWeb do
pipe_through [:browser, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
end
scope "/", CanneryWeb do
pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :confirm
end
end

View File

@ -0,0 +1,55 @@
defmodule CanneryWeb.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("cannery.repo.query.total_time", unit: {:native, :millisecond}),
summary("cannery.repo.query.decode_time", unit: {:native, :millisecond}),
summary("cannery.repo.query.query_time", unit: {:native, :millisecond}),
summary("cannery.repo.query.queue_time", unit: {:native, :millisecond}),
summary("cannery.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.
# {CanneryWeb, :count_users, []}
]
end
end

View File

@ -0,0 +1,19 @@
<main role="main" class="container min-h-full min-w-full">
<header class="mb-4 px-8 py-4 w-full bg-primary-400">
<%= render "topbar.html", assigns %>
<%= if get_flash(@conn, :info) do %>
<p class="alert alert-info" role="alert">
<%= get_flash(@conn, :info) %>
</p>
<% end %>
<%= if get_flash(@conn, :error) do %>
<p class="alert alert-danger" role="alert">
<%= get_flash(@conn, :error) %>
</p>
<% end %>
</header>
<%= @inner_content %>
</main>

View File

@ -0,0 +1,5 @@
<main role="main" class="container min-w-full min-h-full">
<%= live_component CanneryWeb.Live.Component.Topbar %>
<%= @inner_content %>
</main>

View File

@ -0,0 +1,15 @@
<!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] || "Cannery", suffix: "" %>
<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 class="m-0 p-0 min-w-full min-h-full">
<%= @inner_content %>
</body>
</html>

View File

@ -0,0 +1,40 @@
<nav role="navigation">
<div class="flex flex-row justify-between items-center space-x-4">
<%= link to: Routes.page_path(CanneryWeb.Endpoint, :index) do %>
<h1 class="leading-5 text-xl text-white hover:underline">Cannery</h1>
<% end %>
<ul class="flex flex-row flex-wrap justify-center items-center
space-x-4 text-lg text-white">
<%# user settings %>
<%= if assigns |> Map.has_key?(:current_user) && @current_user do %>
<li>
<%= @current_user.email %></li>
<li>
<%= link "Settings", class: "hover:underline",
to: Routes.user_settings_path(CanneryWeb.Endpoint, :edit) %>
</li>
<li>
<%= link "Log out", class: "hover:underline",
to: Routes.user_session_path(CanneryWeb.Endpoint, :delete), method: :delete %>
</li>
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
<li>
<%= link "LiveDashboard", class: "hover:underline",
to: Routes.live_dashboard_path(CanneryWeb.Endpoint, :home) %>
</li>
<% end %>
<% else %>
<li>
<%= link "Register", class: "hover:underline",
to: Routes.user_registration_path(CanneryWeb.Endpoint, :new) %>
</li>
<li>
<%= link "Log in", class: "hover:underline",
to: Routes.user_session_path(CanneryWeb.Endpoint, :new) %>
</li>
<% end %>
</ul>
</div>
</nav>

View File

@ -0,0 +1,24 @@
<div class="flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-500 text-xl">
Resend confirmation instructions
</h1>
<%= form_for :user, Routes.user_confirmation_path(@conn, :create),
[class: "flex flex-col justify-center items-center space-y-4"], fn f -> %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :email, class: "title text-lg text-primary-500" %>
<%= email_input f, :email, required: true, class: "input input-primary col-span-2" %>
</div>
<%= submit "Resend confirmation instructions", class: "btn btn-primary" %>
<hr class="hr">
<div class="flex flex-row justify-center items-center space-x-4">
<%= link "Register", to: Routes.user_registration_path(@conn, :new),
class: "btn btn-primary" %>
<%= link "Log in", to: Routes.user_session_path(@conn, :new),
class: "btn btn-primary" %>
</div>
<% end %>
</div>

View File

@ -0,0 +1,37 @@
<div class="flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-500 text-xl">
Register
</h1>
<%= form_for @changeset, Routes.user_registration_path(@conn, :create),
[class: "flex flex-col justify-center items-center space-y-4"], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :email, class: "title text-lg text-primary-500" %>
<%= email_input f, :email, required: true, class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :email %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :password, class: "title text-lg text-primary-500" %>
<%= password_input f, :password, required: true, class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :password %>
<%= submit "Register", class: "btn btn-primary" %>
<hr class="hr">
<div class="flex flex-row justify-center items-center space-x-4">
<%= link "Log in", to: Routes.user_session_path(@conn, :new),
class: "btn btn-primary" %>
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new),
class: "btn btn-primary" %>
</div>
<% end %>
</div>

View File

@ -0,0 +1,37 @@
<div class="flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-500 text-xl">
Reset password
</h1>
<%= form_for @changeset, Routes.user_reset_password_path(@conn, :update, @token),
[class: "flex flex-col justify-center items-center space-y-4"], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :password, "New password", class: "title text-lg text-primary-500" %>
<%= password_input f, :password, required: true, class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :password %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :password_confirmation, "Confirm new password", class: "title text-lg text-primary-500" %>
<%= password_input f, :password_confirmation, required: true, class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :password_confirmation %>
<%= submit "Reset password", class: "btn btn-primary" %>
<hr class="hr">
<div class="flex flex-row justify-center items-center space-x-4">
<%= link "Register", to: Routes.user_registration_path(@conn, :new),
class: "btn btn-primary" %>
<%= link "Log in", to: Routes.user_session_path(@conn, :new),
class: "btn btn-primary" %>
</div>
<% end %>
</div>

View File

@ -0,0 +1,25 @@
<div class="flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-500 text-xl">
Forgot your password?
</h1>
<%= form_for :user, Routes.user_reset_password_path(@conn, :create),
[class: "flex flex-col justify-center items-center space-y-4"], fn f -> %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :email, class: "title text-lg text-primary-500" %>
<%= email_input f, :email, required: true, class: "input input-primary col-span-2" %>
</div>
<%= submit "Send instructions to reset password", class: "btn btn-primary" %>
<hr class="hr">
<div class="flex flex-row justify-center items-center space-x-4">
<%= link "Register", to: Routes.user_registration_path(@conn, :new),
class: "btn btn-primary" %>
<%= link "Log in", to: Routes.user_session_path(@conn, :new),
class: "btn btn-primary" %>
</div>
<% end %>
</div>

View File

@ -0,0 +1,41 @@
<div class="flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-500 text-xl">
Log in
</h1>
<%= form_for @conn, Routes.user_session_path(@conn, :create), [as: :user,
class: "flex flex-col justify-center items-center space-y-4"], fn f -> %>
<%= if @error_message do %>
<div class="alert alert-danger">
<p><%= @error_message %></p>
</div>
<% end %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :email, class: "title text-lg text-primary-500" %>
<%= email_input f, :email, required: true, class: "input input-primary col-span-2" %>
</div>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :password, class: "title text-lg text-primary-500" %>
<%= password_input f, :password, required: true, class: "input input-primary col-span-2" %>
</div>
<div class="flex flex-row justify-center items-center text-center space-x-4">
<%= label f, :remember_me, "Keep me logged in for 60 days",
class: "title text-lg text-primary-500" %>
<%= checkbox f, :remember_me, class: "checkbox" %>
</div>
<%= submit "Log in", class: "btn btn-primary" %>
<hr class="hr">
<div class="flex flex-row justify-center items-center space-x-4">
<%= link "Register", to: Routes.user_registration_path(@conn, :new),
class: "btn btn-primary" %>
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new),
class: "btn btn-primary" %>
</div>
<% end %>
</div>

View File

@ -0,0 +1,85 @@
<div class="flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-500 text-xl">
Settings
</h1>
<h3 class="title text-primary-500 text-lg">
Change email
</h3>
<%= form_for @email_changeset, Routes.user_settings_path(@conn, :update),
[class: "flex flex-col justify-center items-center space-y-4"], 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 %>
<%= hidden_input f, :action, name: "action", value: "update_email" %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :email, class: "title text-lg text-primary-500" %>
<%= email_input f, :email, required: true, class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :email %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :current_password, for: "current_password_for_email", class: "title text-lg text-primary-500" %>
<%= password_input f, :current_password,
required: true,
name: "current_password",
id: "current_password_for_email",
class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :current_password %>
<%= submit "Change email", class: "btn btn-primary" %>
<% end %>
<h3 class="title text-primary-500 text-lg">
Change password
</h3>
<%= form_for @password_changeset, Routes.user_settings_path(@conn, :update),
[class: "flex flex-col justify-center items-center space-y-4"], 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 %>
<%= hidden_input f, :action, name: "action", value: "update_password" %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :password, "New password",
class: "title text-lg text-primary-500" %>
<%= password_input f, :password,
required: true,
class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :password %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :password_confirmation, "Confirm new password",
class: "title text-lg text-primary-500" %>
<%= password_input f, :password_confirmation,
required: true,
class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :password_confirmation %>
<div class="grid grid-cols-3 justify-center items-center text-center space-x-4">
<%= label f, :current_password,
for: "current_password_for_password",
class: "title text-lg text-primary-500" %>
<%= password_input f, :current_password,
required: true,
name: "current_password",
id: "current_password_for_password",
class: "input input-primary col-span-2" %>
</div>
<%= error_tag f, :current_password %>
<%= submit "Change password", class: "btn btn-primary" %>
<% end %>
</div>

View File

@ -0,0 +1,47 @@
defmodule CanneryWeb.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_name(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(CanneryWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(CanneryWeb.Gettext, "errors", msg, opts)
end
end
end

View File

@ -0,0 +1,16 @@
defmodule CanneryWeb.ErrorView do
use CanneryWeb, :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,11 @@
defmodule CanneryWeb.LayoutView do
use CanneryWeb, :view
def get_title(conn) do
if conn.assigns |> Map.has_key?(:title) do
"Cannery | #{conn.assigns.title}"
else
"Cannery"
end
end
end

View File

@ -0,0 +1,3 @@
defmodule CanneryWeb.PageView do
use CanneryWeb, :view
end

View File

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

View File

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

View File

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

View File

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

View File

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