run phx.new and add phx.gen.auth
This commit is contained in:
		
							
								
								
									
										9
									
								
								lib/lokal.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/lokal.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| defmodule Lokal do | ||||
|   @moduledoc """ | ||||
|   Lokal keeps the contexts that define your domain | ||||
|   and business logic. | ||||
|  | ||||
|   Contexts are also responsible for managing your data, regardless | ||||
|   if it comes from the database, an external API or others. | ||||
|   """ | ||||
| end | ||||
							
								
								
									
										349
									
								
								lib/lokal/accounts.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										349
									
								
								lib/lokal/accounts.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,349 @@ | ||||
| defmodule Lokal.Accounts do | ||||
|   @moduledoc """ | ||||
|   The Accounts context. | ||||
|   """ | ||||
|  | ||||
|   import Ecto.Query, warn: false | ||||
|   alias Lokal.Repo | ||||
|   alias Lokal.Accounts.{User, UserToken, UserNotifier} | ||||
|  | ||||
|   ## Database getters | ||||
|  | ||||
|   @doc """ | ||||
|   Gets a user by email. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> get_user_by_email("foo@example.com") | ||||
|       %User{} | ||||
|  | ||||
|       iex> get_user_by_email("unknown@example.com") | ||||
|       nil | ||||
|  | ||||
|   """ | ||||
|   def get_user_by_email(email) when is_binary(email) do | ||||
|     Repo.get_by(User, email: email) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Gets a user by email and password. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> get_user_by_email_and_password("foo@example.com", "correct_password") | ||||
|       %User{} | ||||
|  | ||||
|       iex> get_user_by_email_and_password("foo@example.com", "invalid_password") | ||||
|       nil | ||||
|  | ||||
|   """ | ||||
|   def get_user_by_email_and_password(email, password) | ||||
|       when is_binary(email) and is_binary(password) do | ||||
|     user = Repo.get_by(User, email: email) | ||||
|     if User.valid_password?(user, password), do: user | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Gets a single user. | ||||
|  | ||||
|   Raises `Ecto.NoResultsError` if the User does not exist. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> get_user!(123) | ||||
|       %User{} | ||||
|  | ||||
|       iex> get_user!(456) | ||||
|       ** (Ecto.NoResultsError) | ||||
|  | ||||
|   """ | ||||
|   def get_user!(id), do: Repo.get!(User, id) | ||||
|  | ||||
|   ## User registration | ||||
|  | ||||
|   @doc """ | ||||
|   Registers a user. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> register_user(%{field: value}) | ||||
|       {:ok, %User{}} | ||||
|  | ||||
|       iex> register_user(%{field: bad_value}) | ||||
|       {:error, %Ecto.Changeset{}} | ||||
|  | ||||
|   """ | ||||
|   def register_user(attrs) do | ||||
|     %User{} | ||||
|     |> User.registration_changeset(attrs) | ||||
|     |> Repo.insert() | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Returns an `%Ecto.Changeset{}` for tracking user changes. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> change_user_registration(user) | ||||
|       %Ecto.Changeset{data: %User{}} | ||||
|  | ||||
|   """ | ||||
|   def change_user_registration(%User{} = user, attrs \\ %{}) do | ||||
|     User.registration_changeset(user, attrs, hash_password: false) | ||||
|   end | ||||
|  | ||||
|   ## Settings | ||||
|  | ||||
|   @doc """ | ||||
|   Returns an `%Ecto.Changeset{}` for changing the user email. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> change_user_email(user) | ||||
|       %Ecto.Changeset{data: %User{}} | ||||
|  | ||||
|   """ | ||||
|   def change_user_email(user, attrs \\ %{}) do | ||||
|     User.email_changeset(user, attrs) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Emulates that the email will change without actually changing | ||||
|   it in the database. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> apply_user_email(user, "valid password", %{email: ...}) | ||||
|       {:ok, %User{}} | ||||
|  | ||||
|       iex> apply_user_email(user, "invalid password", %{email: ...}) | ||||
|       {:error, %Ecto.Changeset{}} | ||||
|  | ||||
|   """ | ||||
|   def apply_user_email(user, password, attrs) do | ||||
|     user | ||||
|     |> User.email_changeset(attrs) | ||||
|     |> User.validate_current_password(password) | ||||
|     |> Ecto.Changeset.apply_action(:update) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Updates the user email using the given token. | ||||
|  | ||||
|   If the token matches, the user email is updated and the token is deleted. | ||||
|   The confirmed_at date is also updated to the current time. | ||||
|   """ | ||||
|   def update_user_email(user, token) do | ||||
|     context = "change:#{user.email}" | ||||
|  | ||||
|     with {:ok, query} <- UserToken.verify_change_email_token_query(token, context), | ||||
|          %UserToken{sent_to: email} <- Repo.one(query), | ||||
|          {:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do | ||||
|       :ok | ||||
|     else | ||||
|       _ -> :error | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp user_email_multi(user, email, context) do | ||||
|     changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset() | ||||
|  | ||||
|     Ecto.Multi.new() | ||||
|     |> Ecto.Multi.update(:user, changeset) | ||||
|     |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, [context])) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Delivers the update email instructions to the given user. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> deliver_update_email_instructions(user, current_email, &Routes.user_update_email_url(conn, :edit, &1)) | ||||
|       {:ok, %{to: ..., body: ...}} | ||||
|  | ||||
|   """ | ||||
|   def deliver_update_email_instructions(%User{} = user, current_email, update_email_url_fun) | ||||
|       when is_function(update_email_url_fun, 1) do | ||||
|     {encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}") | ||||
|  | ||||
|     Repo.insert!(user_token) | ||||
|     UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token)) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Returns an `%Ecto.Changeset{}` for changing the user password. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> change_user_password(user) | ||||
|       %Ecto.Changeset{data: %User{}} | ||||
|  | ||||
|   """ | ||||
|   def change_user_password(user, attrs \\ %{}) do | ||||
|     User.password_changeset(user, attrs, hash_password: false) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Updates the user password. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> update_user_password(user, "valid password", %{password: ...}) | ||||
|       {:ok, %User{}} | ||||
|  | ||||
|       iex> update_user_password(user, "invalid password", %{password: ...}) | ||||
|       {:error, %Ecto.Changeset{}} | ||||
|  | ||||
|   """ | ||||
|   def update_user_password(user, password, attrs) do | ||||
|     changeset = | ||||
|       user | ||||
|       |> User.password_changeset(attrs) | ||||
|       |> User.validate_current_password(password) | ||||
|  | ||||
|     Ecto.Multi.new() | ||||
|     |> Ecto.Multi.update(:user, changeset) | ||||
|     |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all)) | ||||
|     |> Repo.transaction() | ||||
|     |> case do | ||||
|       {:ok, %{user: user}} -> {:ok, user} | ||||
|       {:error, :user, changeset, _} -> {:error, changeset} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   ## Session | ||||
|  | ||||
|   @doc """ | ||||
|   Generates a session token. | ||||
|   """ | ||||
|   def generate_user_session_token(user) do | ||||
|     {token, user_token} = UserToken.build_session_token(user) | ||||
|     Repo.insert!(user_token) | ||||
|     token | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Gets the user with the given signed token. | ||||
|   """ | ||||
|   def get_user_by_session_token(token) do | ||||
|     {:ok, query} = UserToken.verify_session_token_query(token) | ||||
|     Repo.one(query) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Deletes the signed token with the given context. | ||||
|   """ | ||||
|   def delete_session_token(token) do | ||||
|     Repo.delete_all(UserToken.token_and_context_query(token, "session")) | ||||
|     :ok | ||||
|   end | ||||
|  | ||||
|   ## Confirmation | ||||
|  | ||||
|   @doc """ | ||||
|   Delivers the confirmation email instructions to the given user. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> deliver_user_confirmation_instructions(user, &Routes.user_confirmation_url(conn, :confirm, &1)) | ||||
|       {:ok, %{to: ..., body: ...}} | ||||
|  | ||||
|       iex> deliver_user_confirmation_instructions(confirmed_user, &Routes.user_confirmation_url(conn, :confirm, &1)) | ||||
|       {:error, :already_confirmed} | ||||
|  | ||||
|   """ | ||||
|   def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun) | ||||
|       when is_function(confirmation_url_fun, 1) do | ||||
|     if user.confirmed_at do | ||||
|       {:error, :already_confirmed} | ||||
|     else | ||||
|       {encoded_token, user_token} = UserToken.build_email_token(user, "confirm") | ||||
|       Repo.insert!(user_token) | ||||
|       UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token)) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Confirms a user by the given token. | ||||
|  | ||||
|   If the token matches, the user account is marked as confirmed | ||||
|   and the token is deleted. | ||||
|   """ | ||||
|   def confirm_user(token) do | ||||
|     with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"), | ||||
|          %User{} = user <- Repo.one(query), | ||||
|          {:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do | ||||
|       {:ok, user} | ||||
|     else | ||||
|       _ -> :error | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp confirm_user_multi(user) do | ||||
|     Ecto.Multi.new() | ||||
|     |> Ecto.Multi.update(:user, User.confirm_changeset(user)) | ||||
|     |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, ["confirm"])) | ||||
|   end | ||||
|  | ||||
|   ## Reset password | ||||
|  | ||||
|   @doc """ | ||||
|   Delivers the reset password email to the given user. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> deliver_user_reset_password_instructions(user, &Routes.user_reset_password_url(conn, :edit, &1)) | ||||
|       {:ok, %{to: ..., body: ...}} | ||||
|  | ||||
|   """ | ||||
|   def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun) | ||||
|       when is_function(reset_password_url_fun, 1) do | ||||
|     {encoded_token, user_token} = UserToken.build_email_token(user, "reset_password") | ||||
|     Repo.insert!(user_token) | ||||
|     UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token)) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Gets the user by reset password token. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> get_user_by_reset_password_token("validtoken") | ||||
|       %User{} | ||||
|  | ||||
|       iex> get_user_by_reset_password_token("invalidtoken") | ||||
|       nil | ||||
|  | ||||
|   """ | ||||
|   def get_user_by_reset_password_token(token) do | ||||
|     with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"), | ||||
|          %User{} = user <- Repo.one(query) do | ||||
|       user | ||||
|     else | ||||
|       _ -> nil | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Resets the user password. | ||||
|  | ||||
|   ## Examples | ||||
|  | ||||
|       iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"}) | ||||
|       {:ok, %User{}} | ||||
|  | ||||
|       iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"}) | ||||
|       {:error, %Ecto.Changeset{}} | ||||
|  | ||||
|   """ | ||||
|   def reset_user_password(user, attrs) do | ||||
|     Ecto.Multi.new() | ||||
|     |> Ecto.Multi.update(:user, User.password_changeset(user, attrs)) | ||||
|     |> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all)) | ||||
|     |> Repo.transaction() | ||||
|     |> case do | ||||
|       {:ok, %{user: user}} -> {:ok, user} | ||||
|       {:error, :user, changeset, _} -> {:error, changeset} | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										141
									
								
								lib/lokal/accounts/user.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								lib/lokal/accounts/user.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| defmodule Lokal.Accounts.User do | ||||
|   use Ecto.Schema | ||||
|   import Ecto.Changeset | ||||
|  | ||||
|   @derive {Inspect, except: [:password]} | ||||
|   @primary_key {:id, :binary_id, autogenerate: true} | ||||
|   @foreign_key_type :binary_id | ||||
|   schema "users" do | ||||
|     field :email, :string | ||||
|     field :password, :string, virtual: true | ||||
|     field :hashed_password, :string | ||||
|     field :confirmed_at, :naive_datetime | ||||
|  | ||||
|     timestamps() | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   A user changeset for registration. | ||||
|  | ||||
|   It is important to validate the length of both email and password. | ||||
|   Otherwise databases may truncate the email without warnings, which | ||||
|   could lead to unpredictable or insecure behaviour. Long passwords may | ||||
|   also be very expensive to hash for certain algorithms. | ||||
|  | ||||
|   ## Options | ||||
|  | ||||
|     * `:hash_password` - Hashes the password so it can be stored securely | ||||
|       in the database and ensures the password field is cleared to prevent | ||||
|       leaks in the logs. If password hashing is not needed and clearing the | ||||
|       password field is not desired (like when using this changeset for | ||||
|       validations on a LiveView form), this option can be set to `false`. | ||||
|       Defaults to `true`. | ||||
|   """ | ||||
|   def registration_changeset(user, attrs, opts \\ []) do | ||||
|     user | ||||
|     |> cast(attrs, [:email, :password]) | ||||
|     |> validate_email() | ||||
|     |> validate_password(opts) | ||||
|   end | ||||
|  | ||||
|   defp validate_email(changeset) do | ||||
|     changeset | ||||
|     |> validate_required([:email]) | ||||
|     |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces") | ||||
|     |> validate_length(:email, max: 160) | ||||
|     |> unsafe_validate_unique(:email, Lokal.Repo) | ||||
|     |> unique_constraint(:email) | ||||
|   end | ||||
|  | ||||
|   defp validate_password(changeset, opts) do | ||||
|     changeset | ||||
|     |> validate_required([:password]) | ||||
|     |> validate_length(:password, min: 12, max: 80) | ||||
|     # |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character") | ||||
|     # |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character") | ||||
|     # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character") | ||||
|     |> maybe_hash_password(opts) | ||||
|   end | ||||
|  | ||||
|   defp maybe_hash_password(changeset, opts) do | ||||
|     hash_password? = Keyword.get(opts, :hash_password, true) | ||||
|     password = get_change(changeset, :password) | ||||
|  | ||||
|     if hash_password? && password && changeset.valid? do | ||||
|       changeset | ||||
|       |> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password)) | ||||
|       |> delete_change(:password) | ||||
|     else | ||||
|       changeset | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   A user changeset for changing the email. | ||||
|  | ||||
|   It requires the email to change otherwise an error is added. | ||||
|   """ | ||||
|   def email_changeset(user, attrs) do | ||||
|     user | ||||
|     |> cast(attrs, [:email]) | ||||
|     |> validate_email() | ||||
|     |> case do | ||||
|       %{changes: %{email: _}} = changeset -> changeset | ||||
|       %{} = changeset -> add_error(changeset, :email, "did not change") | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   A user changeset for changing the password. | ||||
|  | ||||
|   ## Options | ||||
|  | ||||
|     * `:hash_password` - Hashes the password so it can be stored securely | ||||
|       in the database and ensures the password field is cleared to prevent | ||||
|       leaks in the logs. If password hashing is not needed and clearing the | ||||
|       password field is not desired (like when using this changeset for | ||||
|       validations on a LiveView form), this option can be set to `false`. | ||||
|       Defaults to `true`. | ||||
|   """ | ||||
|   def password_changeset(user, attrs, opts \\ []) do | ||||
|     user | ||||
|     |> cast(attrs, [:password]) | ||||
|     |> validate_confirmation(:password, message: "does not match password") | ||||
|     |> validate_password(opts) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Confirms the account by setting `confirmed_at`. | ||||
|   """ | ||||
|   def confirm_changeset(user) do | ||||
|     now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) | ||||
|     change(user, confirmed_at: now) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Verifies the password. | ||||
|  | ||||
|   If there is no user or the user doesn't have a password, we call | ||||
|   `Bcrypt.no_user_verify/0` to avoid timing attacks. | ||||
|   """ | ||||
|   def valid_password?(%Lokal.Accounts.User{hashed_password: hashed_password}, password) | ||||
|       when is_binary(hashed_password) and byte_size(password) > 0 do | ||||
|     Bcrypt.verify_pass(password, hashed_password) | ||||
|   end | ||||
|  | ||||
|   def valid_password?(_, _) do | ||||
|     Bcrypt.no_user_verify() | ||||
|     false | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Validates the current password otherwise adds an error to the changeset. | ||||
|   """ | ||||
|   def validate_current_password(changeset, password) do | ||||
|     if valid_password?(changeset.data, password) do | ||||
|       changeset | ||||
|     else | ||||
|       add_error(changeset, :current_password, "is not valid") | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										73
									
								
								lib/lokal/accounts/user_notifier.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								lib/lokal/accounts/user_notifier.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| defmodule Lokal.Accounts.UserNotifier do | ||||
|   # For simplicity, this module simply logs messages to the terminal. | ||||
|   # You should replace it by a proper email or notification tool, such as: | ||||
|   # | ||||
|   #   * Swoosh - https://hexdocs.pm/swoosh | ||||
|   #   * Bamboo - https://hexdocs.pm/bamboo | ||||
|   # | ||||
|   defp deliver(to, body) do | ||||
|     require Logger | ||||
|     Logger.debug(body) | ||||
|     {:ok, %{to: to, body: body}} | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Deliver instructions to confirm account. | ||||
|   """ | ||||
|   def deliver_confirmation_instructions(user, url) do | ||||
|     deliver(user.email, """ | ||||
|  | ||||
|     ============================== | ||||
|  | ||||
|     Hi #{user.email}, | ||||
|  | ||||
|     You can confirm your account by visiting the URL below: | ||||
|  | ||||
|     #{url} | ||||
|  | ||||
|     If you didn't create an account with us, please ignore this. | ||||
|  | ||||
|     ============================== | ||||
|     """) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Deliver instructions to reset a user password. | ||||
|   """ | ||||
|   def deliver_reset_password_instructions(user, url) do | ||||
|     deliver(user.email, """ | ||||
|  | ||||
|     ============================== | ||||
|  | ||||
|     Hi #{user.email}, | ||||
|  | ||||
|     You can reset your password by visiting the URL below: | ||||
|  | ||||
|     #{url} | ||||
|  | ||||
|     If you didn't request this change, please ignore this. | ||||
|  | ||||
|     ============================== | ||||
|     """) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Deliver instructions to update a user email. | ||||
|   """ | ||||
|   def deliver_update_email_instructions(user, url) do | ||||
|     deliver(user.email, """ | ||||
|  | ||||
|     ============================== | ||||
|  | ||||
|     Hi #{user.email}, | ||||
|  | ||||
|     You can change your email by visiting the URL below: | ||||
|  | ||||
|     #{url} | ||||
|  | ||||
|     If you didn't request this change, please ignore this. | ||||
|  | ||||
|     ============================== | ||||
|     """) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										141
									
								
								lib/lokal/accounts/user_token.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								lib/lokal/accounts/user_token.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| defmodule Lokal.Accounts.UserToken do | ||||
|   use Ecto.Schema | ||||
|   import Ecto.Query | ||||
|  | ||||
|   @hash_algorithm :sha256 | ||||
|   @rand_size 32 | ||||
|  | ||||
|   # It is very important to keep the reset password token expiry short, | ||||
|   # since someone with access to the email may take over the account. | ||||
|   @reset_password_validity_in_days 1 | ||||
|   @confirm_validity_in_days 7 | ||||
|   @change_email_validity_in_days 7 | ||||
|   @session_validity_in_days 60 | ||||
|  | ||||
|   @primary_key {:id, :binary_id, autogenerate: true} | ||||
|   @foreign_key_type :binary_id | ||||
|   schema "users_tokens" do | ||||
|     field :token, :binary | ||||
|     field :context, :string | ||||
|     field :sent_to, :string | ||||
|     belongs_to :user, Lokal.Accounts.User | ||||
|  | ||||
|     timestamps(updated_at: false) | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Generates a token that will be stored in a signed place, | ||||
|   such as session or cookie. As they are signed, those | ||||
|   tokens do not need to be hashed. | ||||
|   """ | ||||
|   def build_session_token(user) do | ||||
|     token = :crypto.strong_rand_bytes(@rand_size) | ||||
|     {token, %Lokal.Accounts.UserToken{token: token, context: "session", user_id: user.id}} | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Checks if the token is valid and returns its underlying lookup query. | ||||
|  | ||||
|   The query returns the user found by the token. | ||||
|   """ | ||||
|   def verify_session_token_query(token) do | ||||
|     query = | ||||
|       from token in token_and_context_query(token, "session"), | ||||
|         join: user in assoc(token, :user), | ||||
|         where: token.inserted_at > ago(@session_validity_in_days, "day"), | ||||
|         select: user | ||||
|  | ||||
|     {:ok, query} | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Builds a token with a hashed counter part. | ||||
|  | ||||
|   The non-hashed token is sent to the user email while the | ||||
|   hashed part is stored in the database, to avoid reconstruction. | ||||
|   The token is valid for a week as long as users don't change | ||||
|   their email. | ||||
|   """ | ||||
|   def build_email_token(user, context) do | ||||
|     build_hashed_token(user, context, user.email) | ||||
|   end | ||||
|  | ||||
|   defp build_hashed_token(user, context, sent_to) do | ||||
|     token = :crypto.strong_rand_bytes(@rand_size) | ||||
|     hashed_token = :crypto.hash(@hash_algorithm, token) | ||||
|  | ||||
|     {Base.url_encode64(token, padding: false), | ||||
|      %Lokal.Accounts.UserToken{ | ||||
|        token: hashed_token, | ||||
|        context: context, | ||||
|        sent_to: sent_to, | ||||
|        user_id: user.id | ||||
|      }} | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Checks if the token is valid and returns its underlying lookup query. | ||||
|  | ||||
|   The query returns the user found by the token. | ||||
|   """ | ||||
|   def verify_email_token_query(token, context) do | ||||
|     case Base.url_decode64(token, padding: false) do | ||||
|       {:ok, decoded_token} -> | ||||
|         hashed_token = :crypto.hash(@hash_algorithm, decoded_token) | ||||
|         days = days_for_context(context) | ||||
|  | ||||
|         query = | ||||
|           from token in token_and_context_query(hashed_token, context), | ||||
|             join: user in assoc(token, :user), | ||||
|             where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email, | ||||
|             select: user | ||||
|  | ||||
|         {:ok, query} | ||||
|  | ||||
|       :error -> | ||||
|         :error | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp days_for_context("confirm"), do: @confirm_validity_in_days | ||||
|   defp days_for_context("reset_password"), do: @reset_password_validity_in_days | ||||
|  | ||||
|   @doc """ | ||||
|   Checks if the token is valid and returns its underlying lookup query. | ||||
|  | ||||
|   The query returns the user token record. | ||||
|   """ | ||||
|   def verify_change_email_token_query(token, context) do | ||||
|     case Base.url_decode64(token, padding: false) do | ||||
|       {:ok, decoded_token} -> | ||||
|         hashed_token = :crypto.hash(@hash_algorithm, decoded_token) | ||||
|  | ||||
|         query = | ||||
|           from token in token_and_context_query(hashed_token, context), | ||||
|             where: token.inserted_at > ago(@change_email_validity_in_days, "day") | ||||
|  | ||||
|         {:ok, query} | ||||
|  | ||||
|       :error -> | ||||
|         :error | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Returns the given token with the given context. | ||||
|   """ | ||||
|   def token_and_context_query(token, context) do | ||||
|     from Lokal.Accounts.UserToken, where: [token: ^token, context: ^context] | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   Gets all tokens for the given user for the given contexts. | ||||
|   """ | ||||
|   def user_and_contexts_query(user, :all) do | ||||
|     from t in Lokal.Accounts.UserToken, where: t.user_id == ^user.id | ||||
|   end | ||||
|  | ||||
|   def user_and_contexts_query(user, [_ | _] = contexts) do | ||||
|     from t in Lokal.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts | ||||
|   end | ||||
| end | ||||
							
								
								
									
										34
									
								
								lib/lokal/application.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								lib/lokal/application.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| defmodule Lokal.Application do | ||||
|   # See https://hexdocs.pm/elixir/Application.html | ||||
|   # for more information on OTP Applications | ||||
|   @moduledoc false | ||||
|  | ||||
|   use Application | ||||
|  | ||||
|   def start(_type, _args) do | ||||
|     children = [ | ||||
|       # Start the Ecto repository | ||||
|       Lokal.Repo, | ||||
|       # Start the Telemetry supervisor | ||||
|       LokalWeb.Telemetry, | ||||
|       # Start the PubSub system | ||||
|       {Phoenix.PubSub, name: Lokal.PubSub}, | ||||
|       # Start the Endpoint (http/https) | ||||
|       LokalWeb.Endpoint | ||||
|       # Start a worker by calling: Lokal.Worker.start_link(arg) | ||||
|       # {Lokal.Worker, arg} | ||||
|     ] | ||||
|  | ||||
|     # See https://hexdocs.pm/elixir/Supervisor.html | ||||
|     # for other strategies and supported options | ||||
|     opts = [strategy: :one_for_one, name: Lokal.Supervisor] | ||||
|     Supervisor.start_link(children, opts) | ||||
|   end | ||||
|  | ||||
|   # Tell Phoenix to update the endpoint configuration | ||||
|   # whenever the application is updated. | ||||
|   def config_change(changed, _new, removed) do | ||||
|     LokalWeb.Endpoint.config_change(changed, removed) | ||||
|     :ok | ||||
|   end | ||||
| end | ||||
							
								
								
									
										5
									
								
								lib/lokal/repo.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/lokal/repo.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| defmodule Lokal.Repo do | ||||
|   use Ecto.Repo, | ||||
|     otp_app: :lokal, | ||||
|     adapter: Ecto.Adapters.Postgres | ||||
| end | ||||
							
								
								
									
										102
									
								
								lib/lokal_web.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								lib/lokal_web.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| defmodule LokalWeb do | ||||
|   @moduledoc """ | ||||
|   The entrypoint for defining your web interface, such | ||||
|   as controllers, views, channels and so on. | ||||
|  | ||||
|   This can be used in your application as: | ||||
|  | ||||
|       use LokalWeb, :controller | ||||
|       use LokalWeb, :view | ||||
|  | ||||
|   The definitions below will be executed for every view, | ||||
|   controller, etc, so keep them short and clean, focused | ||||
|   on imports, uses and aliases. | ||||
|  | ||||
|   Do NOT define functions inside the quoted expressions | ||||
|   below. Instead, define any helper function in modules | ||||
|   and import those modules here. | ||||
|   """ | ||||
|  | ||||
|   def controller do | ||||
|     quote do | ||||
|       use Phoenix.Controller, namespace: LokalWeb | ||||
|  | ||||
|       import Plug.Conn | ||||
|       import LokalWeb.Gettext | ||||
|       alias LokalWeb.Router.Helpers, as: Routes | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def view do | ||||
|     quote do | ||||
|       use Phoenix.View, | ||||
|         root: "lib/lokal_web/templates", | ||||
|         namespace: LokalWeb | ||||
|  | ||||
|       # Import convenience functions from controllers | ||||
|       import Phoenix.Controller, | ||||
|         only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] | ||||
|  | ||||
|       # Include shared imports and aliases for views | ||||
|       unquote(view_helpers()) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def live_view do | ||||
|     quote do | ||||
|       use Phoenix.LiveView, | ||||
|         layout: {LokalWeb.LayoutView, "live.html"} | ||||
|  | ||||
|       unquote(view_helpers()) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def live_component do | ||||
|     quote do | ||||
|       use Phoenix.LiveComponent | ||||
|  | ||||
|       unquote(view_helpers()) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def router do | ||||
|     quote do | ||||
|       use Phoenix.Router | ||||
|  | ||||
|       import Plug.Conn | ||||
|       import Phoenix.Controller | ||||
|       import Phoenix.LiveView.Router | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def channel do | ||||
|     quote do | ||||
|       use Phoenix.Channel | ||||
|       import LokalWeb.Gettext | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp view_helpers do | ||||
|     quote do | ||||
|       # Use all HTML functionality (forms, tags, etc) | ||||
|       use Phoenix.HTML | ||||
|  | ||||
|       # Import LiveView helpers (live_render, live_component, live_patch, etc) | ||||
|       import Phoenix.LiveView.Helpers | ||||
|  | ||||
|       # Import basic rendering functionality (render, render_layout, etc) | ||||
|       import Phoenix.View | ||||
|  | ||||
|       import LokalWeb.ErrorHelpers | ||||
|       import LokalWeb.Gettext | ||||
|       alias LokalWeb.Router.Helpers, as: Routes | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   @doc """ | ||||
|   When used, dispatch to the appropriate controller/view/etc. | ||||
|   """ | ||||
|   defmacro __using__(which) when is_atom(which) do | ||||
|     apply(__MODULE__, which, []) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										35
									
								
								lib/lokal_web/channels/user_socket.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								lib/lokal_web/channels/user_socket.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| defmodule LokalWeb.UserSocket do | ||||
|   use Phoenix.Socket | ||||
|  | ||||
|   ## Channels | ||||
|   # channel "room:*", LokalWeb.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: | ||||
|   # | ||||
|   #     LokalWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) | ||||
|   # | ||||
|   # Returning `nil` makes this socket anonymous. | ||||
|   @impl true | ||||
|   def id(_socket), do: nil | ||||
| end | ||||
							
								
								
									
										7
									
								
								lib/lokal_web/controllers/page_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib/lokal_web/controllers/page_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| defmodule LokalWeb.PageController do | ||||
|   use LokalWeb, :controller | ||||
|  | ||||
|   def index(conn, _params) do | ||||
|     render(conn, "index.html") | ||||
|   end | ||||
| end | ||||
							
								
								
									
										149
									
								
								lib/lokal_web/controllers/user_auth.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								lib/lokal_web/controllers/user_auth.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| defmodule LokalWeb.UserAuth do | ||||
|   import Plug.Conn | ||||
|   import Phoenix.Controller | ||||
|  | ||||
|   alias Lokal.Accounts | ||||
|   alias LokalWeb.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 "_lokal_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 | ||||
|       LokalWeb.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 | ||||
							
								
								
									
										53
									
								
								lib/lokal_web/controllers/user_confirmation_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/lokal_web/controllers/user_confirmation_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| defmodule LokalWeb.UserConfirmationController do | ||||
|   use LokalWeb, :controller | ||||
|  | ||||
|   alias Lokal.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 | ||||
							
								
								
									
										30
									
								
								lib/lokal_web/controllers/user_registration_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lib/lokal_web/controllers/user_registration_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| defmodule LokalWeb.UserRegistrationController do | ||||
|   use LokalWeb, :controller | ||||
|  | ||||
|   alias Lokal.Accounts | ||||
|   alias Lokal.Accounts.User | ||||
|   alias LokalWeb.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 | ||||
							
								
								
									
										59
									
								
								lib/lokal_web/controllers/user_reset_password_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								lib/lokal_web/controllers/user_reset_password_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| defmodule LokalWeb.UserResetPasswordController do | ||||
|   use LokalWeb, :controller | ||||
|  | ||||
|   alias Lokal.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 | ||||
							
								
								
									
										26
									
								
								lib/lokal_web/controllers/user_session_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/lokal_web/controllers/user_session_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| defmodule LokalWeb.UserSessionController do | ||||
|   use LokalWeb, :controller | ||||
|  | ||||
|   alias Lokal.Accounts | ||||
|   alias LokalWeb.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 | ||||
							
								
								
									
										74
									
								
								lib/lokal_web/controllers/user_settings_controller.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								lib/lokal_web/controllers/user_settings_controller.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| defmodule LokalWeb.UserSettingsController do | ||||
|   use LokalWeb, :controller | ||||
|  | ||||
|   alias Lokal.Accounts | ||||
|   alias LokalWeb.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 | ||||
							
								
								
									
										54
									
								
								lib/lokal_web/endpoint.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								lib/lokal_web/endpoint.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| defmodule LokalWeb.Endpoint do | ||||
|   use Phoenix.Endpoint, otp_app: :lokal | ||||
|  | ||||
|   # 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: "_lokal_key", | ||||
|     signing_salt: "fxAnJltS" | ||||
|   ] | ||||
|  | ||||
|   socket "/socket", LokalWeb.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: :lokal, | ||||
|     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: :lokal | ||||
|   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 LokalWeb.Router | ||||
| end | ||||
							
								
								
									
										24
									
								
								lib/lokal_web/gettext.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/lokal_web/gettext.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| defmodule LokalWeb.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 LokalWeb.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: :lokal | ||||
| end | ||||
							
								
								
									
										75
									
								
								lib/lokal_web/live/component/topbar.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								lib/lokal_web/live/component/topbar.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| defmodule LokalWeb.Live.Component.Topbar do | ||||
|   use LokalWeb, :live_component | ||||
|    | ||||
|   def mount(socket), do: {:ok, socket |> assign(results: [])} | ||||
|  | ||||
|   def render(assigns) do | ||||
|     ~L""" | ||||
|     <header class="mb-4 px-8 py-4 w-full bg-primary-400"> | ||||
|       <nav role="navigation"> | ||||
|         <div class="flex flex-row justify-between items-center space-x-4"> | ||||
|           <h1 class="leading-5 text-xl text-white">Lokal</h1> | ||||
|          | ||||
|           <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" | ||||
|                 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(LokalWeb.Endpoint, :edit) %> | ||||
|               </li> | ||||
|               <li> | ||||
|                 <%= link "Log out", class: "hover:underline", | ||||
|                   to: Routes.user_session_path(LokalWeb.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(LokalWeb.Endpoint, :home) %> | ||||
|                 </li> | ||||
|               <% end %> | ||||
|             <% else %> | ||||
|               <li> | ||||
|                 <%= link "Register", class: "hover:underline", | ||||
|                   to: Routes.user_registration_path(LokalWeb.Endpoint, :new) %> | ||||
|               </li> | ||||
|               <li> | ||||
|                 <%= link "Log in", class: "hover:underline", | ||||
|                   to: Routes.user_session_path(LokalWeb.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 | ||||
							
								
								
									
										39
									
								
								lib/lokal_web/live/page_live.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								lib/lokal_web/live/page_live.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| defmodule LokalWeb.PageLive do | ||||
|   use LokalWeb, :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 LokalWeb.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 | ||||
							
								
								
									
										5
									
								
								lib/lokal_web/live/page_live.html.leex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/lokal_web/live/page_live.html.leex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| <div class="flex flex-col justify-center items-center text-center"> | ||||
|   <p> | ||||
|     Welcome to Lokal! | ||||
|   </p> | ||||
| </div> | ||||
							
								
								
									
										73
									
								
								lib/lokal_web/router.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								lib/lokal_web/router.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| defmodule LokalWeb.Router do | ||||
|   use LokalWeb, :router | ||||
|  | ||||
|   import LokalWeb.UserAuth | ||||
|  | ||||
|   pipeline :browser do | ||||
|     plug :accepts, ["html"] | ||||
|     plug :fetch_session | ||||
|     plug :fetch_live_flash | ||||
|     plug :put_root_layout, {LokalWeb.LayoutView, :root} | ||||
|     plug :protect_from_forgery | ||||
|     plug :put_secure_browser_headers | ||||
|     plug :fetch_current_user | ||||
|   end | ||||
|  | ||||
|   pipeline :api do | ||||
|     plug :accepts, ["json"] | ||||
|   end | ||||
|  | ||||
|   scope "/", LokalWeb 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: LokalWeb.Telemetry | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   ## Authentication routes | ||||
|  | ||||
|   scope "/", LokalWeb 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 "/", LokalWeb 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 "/", LokalWeb 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 | ||||
							
								
								
									
										55
									
								
								lib/lokal_web/telemetry.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								lib/lokal_web/telemetry.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| defmodule LokalWeb.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("lokal.repo.query.total_time", unit: {:native, :millisecond}), | ||||
|       summary("lokal.repo.query.decode_time", unit: {:native, :millisecond}), | ||||
|       summary("lokal.repo.query.query_time", unit: {:native, :millisecond}), | ||||
|       summary("lokal.repo.query.queue_time", unit: {:native, :millisecond}), | ||||
|       summary("lokal.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. | ||||
|       # {LokalWeb, :count_users, []} | ||||
|     ] | ||||
|   end | ||||
| end | ||||
							
								
								
									
										11
									
								
								lib/lokal_web/templates/layout/app.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/lokal_web/templates/layout/app.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| <main role="main" class="container min-h-full min-w-full"> | ||||
|   <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> | ||||
							
								
								
									
										5
									
								
								lib/lokal_web/templates/layout/live.html.leex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								lib/lokal_web/templates/layout/live.html.leex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| <main role="main" class="container min-w-full min-h-full"> | ||||
|   <%= live_component LokalWeb.Live.Component.Topbar %> | ||||
|  | ||||
|   <%= @inner_content %> | ||||
| </main> | ||||
							
								
								
									
										15
									
								
								lib/lokal_web/templates/layout/root.html.leex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/lokal_web/templates/layout/root.html.leex
									
									
									
									
									
										Normal 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] || "Lokal", 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> | ||||
							
								
								
									
										4
									
								
								lib/lokal_web/templates/page/index.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								lib/lokal_web/templates/page/index.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| <div class="flex flex-col space-y-8 text-center"> | ||||
|   <h1 class="">Welcome to Lokal</h1> | ||||
|   <p>Shop from your community</p> | ||||
| </div> | ||||
							
								
								
									
										15
									
								
								lib/lokal_web/templates/user_confirmation/new.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/lokal_web/templates/user_confirmation/new.html.eex
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										26
									
								
								lib/lokal_web/templates/user_registration/new.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/lokal_web/templates/user_registration/new.html.eex
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										26
									
								
								lib/lokal_web/templates/user_reset_password/edit.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								lib/lokal_web/templates/user_reset_password/edit.html.eex
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										15
									
								
								lib/lokal_web/templates/user_reset_password/new.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								lib/lokal_web/templates/user_reset_password/new.html.eex
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										27
									
								
								lib/lokal_web/templates/user_session/new.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/lokal_web/templates/user_session/new.html.eex
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										53
									
								
								lib/lokal_web/templates/user_settings/edit.html.eex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/lokal_web/templates/user_settings/edit.html.eex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| <h1>Settings</h1> | ||||
|  | ||||
| <h3>Change email</h3> | ||||
|  | ||||
| <%= form_for @email_changeset, Routes.user_settings_path(@conn, :update), 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" %> | ||||
|  | ||||
|   <%= 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), 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" %> | ||||
|  | ||||
|   <%= 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 %> | ||||
							
								
								
									
										47
									
								
								lib/lokal_web/views/error_helpers.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/lokal_web/views/error_helpers.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| defmodule LokalWeb.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(LokalWeb.Gettext, "errors", msg, msg, count, opts) | ||||
|     else | ||||
|       Gettext.dgettext(LokalWeb.Gettext, "errors", msg, opts) | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										16
									
								
								lib/lokal_web/views/error_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/lokal_web/views/error_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| defmodule LokalWeb.ErrorView do | ||||
|   use LokalWeb, :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 | ||||
							
								
								
									
										11
									
								
								lib/lokal_web/views/layout_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/lokal_web/views/layout_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| defmodule LokalWeb.LayoutView do | ||||
|   use LokalWeb, :view | ||||
|    | ||||
|   def get_title(conn) do | ||||
|     if conn.assigns |> Map.has_key?(:title) do | ||||
|       "Lokal | #{conn.assigns.title}" | ||||
|     else | ||||
|       "Lokal" | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/page_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/page_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule LokalWeb.PageView do | ||||
|   use LokalWeb, :view | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_confirmation_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_confirmation_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule LokalWeb.UserConfirmationView do | ||||
|   use LokalWeb, :view | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_registration_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_registration_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule LokalWeb.UserRegistrationView do | ||||
|   use LokalWeb, :view | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_reset_password_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_reset_password_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule LokalWeb.UserResetPasswordView do | ||||
|   use LokalWeb, :view | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_session_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_session_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule LokalWeb.UserSessionView do | ||||
|   use LokalWeb, :view | ||||
| end | ||||
							
								
								
									
										3
									
								
								lib/lokal_web/views/user_settings_view.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								lib/lokal_web/views/user_settings_view.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| defmodule LokalWeb.UserSettingsView do | ||||
|   use LokalWeb, :view | ||||
| end | ||||
		Reference in New Issue
	
	Block a user