Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/user registration #18

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@
@import "tailwindcss/utilities";

/* This file is for your main application CSS */

.signupbackground {
background-image: url("/images/signupbg.svg");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
height: 100vh;
}
3 changes: 3 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import Config

# Only in tests, remove the complexity from the password hashing algorithm
config :bcrypt_elixir, :log_rounds, 1

# Configure your database
#
# The MIX_TEST_PARTITION environment variable can be used
Expand Down
353 changes: 353 additions & 0 deletions lib/elixir_conf_africa/users.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
defmodule ElixirConfAfrica.Users do
@moduledoc """
The Users context.
"""

import Ecto.Query, warn: false
alias ElixirConfAfrica.Repo

alias ElixirConfAfrica.Users.{User, UserToken, UserNotifier}

## Database getters

@doc """
Gets a user by email.

## Examples

iex> get_user_by_email("[email protected]")
%User{}

iex> get_user_by_email("[email protected]")
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("[email protected]", "correct_password")
%User{}

iex> get_user_by_email_and_password("[email protected]", "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, validate_email: 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, validate_email: false)
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 ~S"""
Delivers the update email instructions to the given user.

## Examples

iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm_email/#{&1})")
{:ok, %{to: ..., body: ...}}

"""
def deliver_user_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_user_session_token(token) do
Repo.delete_all(UserToken.token_and_context_query(token, "session"))
:ok
end

## Confirmation

@doc ~S"""
Delivers the confirmation email instructions to the given user.

## Examples

iex> deliver_user_confirmation_instructions(user, &url(~p"/users/confirm/#{&1}"))
{:ok, %{to: ..., body: ...}}

iex> deliver_user_confirmation_instructions(confirmed_user, &url(~p"/users/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 ~S"""
Delivers the reset password email to the given user.

## Examples

iex> deliver_user_reset_password_instructions(user, &url(~p"/users/reset_password/#{&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
Loading