Skip to content
Merged
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
264 changes: 134 additions & 130 deletions lib/cklist/accounts.ex

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions lib/cklist/accounts/users.ex → lib/cklist/accounts/user.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Cklist.Accounts.Users do
defmodule Cklist.Accounts.User do
use Ecto.Schema
import Ecto.Changeset

Expand All @@ -12,7 +12,7 @@ defmodule Cklist.Accounts.Users do
end

@doc """
A users changeset for registration.
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
Expand All @@ -34,8 +34,8 @@ defmodule Cklist.Accounts.Users do
submitting the form), this option can be set to `false`.
Defaults to `true`.
"""
def registration_changeset(users, attrs, opts \\ []) do
users
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :password])
|> validate_email(opts)
|> validate_password(opts)
Expand Down Expand Up @@ -88,12 +88,12 @@ defmodule Cklist.Accounts.Users do
end

@doc """
A users changeset for changing the email.
A user changeset for changing the email.

It requires the email to change otherwise an error is added.
"""
def email_changeset(users, attrs, opts \\ []) do
users
def email_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email])
|> validate_email(opts)
|> case do
Expand All @@ -103,7 +103,7 @@ defmodule Cklist.Accounts.Users do
end

@doc """
A users changeset for changing the password.
A user changeset for changing the password.

## Options

Expand All @@ -114,8 +114,8 @@ defmodule Cklist.Accounts.Users do
validations on a LiveView form), this option can be set to `false`.
Defaults to `true`.
"""
def password_changeset(users, attrs, opts \\ []) do
users
def password_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:password])
|> validate_confirmation(:password, message: "does not match password")
|> validate_password(opts)
Expand All @@ -124,18 +124,18 @@ defmodule Cklist.Accounts.Users do
@doc """
Confirms the account by setting `confirmed_at`.
"""
def confirm_changeset(users) do
def confirm_changeset(user) do
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
change(users, confirmed_at: now)
change(user, confirmed_at: now)
end

@doc """
Verifies the password.

If there is no users or the users doesn't have a password, we call
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?(%Cklist.Accounts.Users{hashed_password: hashed_password}, password)
def valid_password?(%Cklist.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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Cklist.Accounts.UsersNotifier do
defmodule Cklist.Accounts.UserNotifier do
import Swoosh.Email

alias Cklist.Mailer
Expand All @@ -20,12 +20,12 @@ defmodule Cklist.Accounts.UsersNotifier do
@doc """
Deliver instructions to confirm account.
"""
def deliver_confirmation_instructions(users, url) do
deliver(users.email, "Confirmation instructions", """
def deliver_confirmation_instructions(user, url) do
deliver(user.email, "Confirmation instructions", """

==============================

Hi #{users.email},
Hi #{user.email},

You can confirm your account by visiting the URL below:

Expand All @@ -38,14 +38,14 @@ defmodule Cklist.Accounts.UsersNotifier do
end

@doc """
Deliver instructions to reset a users password.
Deliver instructions to reset a user password.
"""
def deliver_reset_password_instructions(users, url) do
deliver(users.email, "Reset password instructions", """
def deliver_reset_password_instructions(user, url) do
deliver(user.email, "Reset password instructions", """

==============================

Hi #{users.email},
Hi #{user.email},

You can reset your password by visiting the URL below:

Expand All @@ -58,14 +58,14 @@ defmodule Cklist.Accounts.UsersNotifier do
end

@doc """
Deliver instructions to update a users email.
Deliver instructions to update a user email.
"""
def deliver_update_email_instructions(users, url) do
deliver(users.email, "Update email instructions", """
def deliver_update_email_instructions(user, url) do
deliver(user.email, "Update email instructions", """

==============================

Hi #{users.email},
Hi #{user.email},

You can change your email by visiting the URL below:

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Cklist.Accounts.UsersToken do
defmodule Cklist.Accounts.UserToken do
use Ecto.Schema
import Ecto.Query
alias Cklist.Accounts.UsersToken
alias Cklist.Accounts.UserToken

@hash_algorithm :sha256
@rand_size 32
Expand All @@ -13,11 +13,11 @@ defmodule Cklist.Accounts.UsersToken do
@change_email_validity_in_days 7
@session_validity_in_days 60

schema "users_tokens" do
schema "user_tokens" do
field :token, :binary
field :context, :string
field :sent_to, :string
belongs_to :users, Cklist.Accounts.Users
belongs_to :user, Cklist.Accounts.User

timestamps(updated_at: false)
end
Expand All @@ -34,70 +34,70 @@ defmodule Cklist.Accounts.UsersToken do
valid indefinitely, unless you change the signing/encryption
salt.

Therefore, storing them allows individual users
Therefore, storing them allows individual user
sessions to be expired. The token system can also be extended
to store additional data, such as the device used for logging in.
You could then use this information to display all valid sessions
and devices in the UI and allow users to explicitly expire any
and devices in the UI and allow user to explicitly expire any
session they deem invalid.
"""
def build_session_token(users) do
def build_session_token(user) do
token = :crypto.strong_rand_bytes(@rand_size)
{token, %UsersToken{token: token, context: "session", users_id: users.id}}
{token, %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 users found by the token, if any.
The query returns the user found by the token, if any.

The token is valid if it matches the value in the database and it has
not expired (after @session_validity_in_days).
"""
def verify_session_token_query(token) do
query =
from token in by_token_and_context_query(token, "session"),
join: users in assoc(token, :users),
join: user in assoc(token, :user),
where: token.inserted_at > ago(@session_validity_in_days, "day"),
select: users
select: user

{:ok, query}
end

@doc """
Builds a token and its hash to be delivered to the users's email.
Builds a token and its hash to be delivered to the user's email.

The non-hashed token is sent to the users email while the
The non-hashed token is sent to the user email while the
hashed part is stored in the database. The original token cannot be reconstructed,
which means anyone with read-only access to the database cannot directly use
the token in the application to gain access. Furthermore, if the user changes
their email in the system, the tokens sent to the previous email are no longer
valid.

Users can easily adapt the existing code to provide other types of delivery methods,
User can easily adapt the existing code to provide other types of delivery methods,
for example, by phone numbers.
"""
def build_email_token(users, context) do
build_hashed_token(users, context, users.email)
def build_email_token(user, context) do
build_hashed_token(user, context, user.email)
end

defp build_hashed_token(users, context, sent_to) do
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),
%UsersToken{
%UserToken{
token: hashed_token,
context: context,
sent_to: sent_to,
users_id: users.id
user_id: user.id
}}
end

@doc """
Checks if the token is valid and returns its underlying lookup query.

The query returns the users found by the token, if any.
The query returns the user found by the token, if any.

The given token is valid if it matches its hashed counterpart in the
database and the user email has not changed. This function also checks
Expand All @@ -115,9 +115,9 @@ defmodule Cklist.Accounts.UsersToken do

query =
from token in by_token_and_context_query(hashed_token, context),
join: users in assoc(token, :users),
where: token.inserted_at > ago(^days, "day") and token.sent_to == users.email,
select: users
join: user in assoc(token, :user),
where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email,
select: user

{:ok, query}

Expand All @@ -132,9 +132,9 @@ defmodule Cklist.Accounts.UsersToken do
@doc """
Checks if the token is valid and returns its underlying lookup query.

The query returns the users found by the token, if any.
The query returns the user found by the token, if any.

This is used to validate requests to change the users
This is used to validate requests to change the user
email. It is different from `verify_email_token_query/2` precisely because
`verify_email_token_query/2` validates the email has not changed, which is
the starting point by this function.
Expand Down Expand Up @@ -163,17 +163,17 @@ defmodule Cklist.Accounts.UsersToken do
Returns the token struct for the given token value and context.
"""
def by_token_and_context_query(token, context) do
from UsersToken, where: [token: ^token, context: ^context]
from UserToken, where: [token: ^token, context: ^context]
end

@doc """
Gets all tokens for the given users for the given contexts.
Gets all tokens for the given user for the given contexts.
"""
def by_users_and_contexts_query(users, :all) do
from t in UsersToken, where: t.users_id == ^users.id
def by_user_and_contexts_query(user, :all) do
from t in UserToken, where: t.user_id == ^user.id
end

def by_users_and_contexts_query(users, [_ | _] = contexts) do
from t in UsersToken, where: t.users_id == ^users.id and t.context in ^contexts
def by_user_and_contexts_query(user, [_ | _] = contexts) do
from t in UserToken, where: t.user_id == ^user.id and t.context in ^contexts
end
end
2 changes: 1 addition & 1 deletion lib/cklist_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ defmodule CklistWeb.CoreComponents do

## Examples

<.table id="users" rows={@users}>
<.table id="user" rows={@user}>
<:col :let={user} label="id"><%= user.id %></:col>
<:col :let={user} label="username"><%= user.username %></:col>
</.table>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
defmodule CklistWeb.UsersSessionController do
defmodule CklistWeb.UserSessionController do
use CklistWeb, :controller

alias Cklist.Accounts
alias CklistWeb.UsersAuth
alias CklistWeb.UserAuth

def create(conn, %{"_action" => "registered"} = params) do
create(conn, params, "Account created successfully!")
end

def create(conn, %{"_action" => "password_updated"} = params) do
conn
|> put_session(:users_return_to, ~p"/users/settings")
|> put_session(:user_return_to, ~p"/user/settings")
|> create(params, "Password updated successfully!")
end

def create(conn, params) do
create(conn, params, "Welcome back!")
end

defp create(conn, %{"users" => users_params}, info) do
%{"email" => email, "password" => password} = users_params
defp create(conn, %{"user" => user_params}, info) do
%{"email" => email, "password" => password} = user_params

if users = Accounts.get_users_by_email_and_password(email, password) do
if user = Accounts.get_user_by_email_and_password(email, password) do
conn
|> put_flash(:info, info)
|> UsersAuth.log_in_users(users, users_params)
|> UserAuth.log_in_user(user, user_params)
else
# In order to prevent user enumeration attacks, don't disclose whether the email is registered.
conn
|> put_flash(:error, "Invalid email or password")
|> put_flash(:email, String.slice(email, 0, 160))
|> redirect(to: ~p"/users/log_in")
|> redirect(to: ~p"/user/log_in")
end
end

def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> UsersAuth.log_out_users()
|> UserAuth.log_out_user()
end
end
Loading