-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Add everything related to user login and authentication. Request them for every settings route * Add tests for user authentication modules * Add Configurator landing page aka home * Add styles to every rendered template in Configurator * Delete unused code from phoenix auth * Fix mix credo * Fix mix credo * Delete deleted code tests and create auth user for character controller tests
- Loading branch information
1 parent
753a762
commit 1c0441c
Showing
24 changed files
with
1,258 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
defmodule Configurator.Accounts do | ||
@moduledoc """ | ||
The Accounts context. | ||
""" | ||
|
||
alias Configurator.Repo | ||
alias Configurator.Accounts.User | ||
alias Configurator.Accounts.UserToken | ||
|
||
@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 | ||
|
||
## 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 | ||
|
||
## 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.by_token_and_context_query(token, "session")) | ||
:ok | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
defmodule Configurator.Accounts.User do | ||
@moduledoc false | ||
use Configurator.Schema | ||
import Ecto.Changeset | ||
|
||
schema "users" do | ||
field :email, :string | ||
field :password, :string, virtual: true, redact: true | ||
field :hashed_password, :string, redact: true | ||
field :confirmed_at, :naive_datetime | ||
|
||
timestamps(type: :utc_datetime) | ||
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`. | ||
* `:validate_email` - Validates the uniqueness of the email, in case | ||
you don't want to validate the uniqueness of the email (like when | ||
using this changeset for validations on a LiveView form before | ||
submitting the 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(opts) | ||
|> validate_password(opts) | ||
end | ||
|
||
defp validate_email(changeset, opts) do | ||
changeset | ||
|> validate_required([:email]) | ||
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces") | ||
|> validate_length(:email, max: 160) | ||
|> maybe_validate_unique_email(opts) | ||
end | ||
|
||
defp validate_password(changeset, opts) do | ||
changeset | ||
|> validate_required([:password]) | ||
|> validate_length(:password, min: 12, max: 72) | ||
# Examples of additional password validation: | ||
# |> 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 | ||
# If using Bcrypt, then further validate it is at most 72 bytes long | ||
|> validate_length(:password, max: 72, count: :bytes) | ||
# Hashing could be done with `Ecto.Changeset.prepare_changes/2`, but that | ||
# would keep the database transaction open longer and hurt performance. | ||
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password)) | ||
|> delete_change(:password) | ||
else | ||
changeset | ||
end | ||
end | ||
|
||
defp maybe_validate_unique_email(changeset, opts) do | ||
if Keyword.get(opts, :validate_email, true) do | ||
changeset | ||
|> unsafe_validate_unique(:email, Configurator.Repo) | ||
|> unique_constraint(:email) | ||
else | ||
changeset | ||
end | ||
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?(%Configurator.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 | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
defmodule Configurator.Accounts.UserToken do | ||
@moduledoc false | ||
use Configurator.Schema | ||
import Ecto.Query | ||
alias Configurator.Accounts.UserToken | ||
|
||
@rand_size 32 | ||
@session_validity_in_days 60 | ||
|
||
schema "users_tokens" do | ||
field :token, :binary | ||
field :context, :string | ||
field :sent_to, :string | ||
belongs_to :user, Configurator.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. | ||
The reason why we store session tokens in the database, even | ||
though Phoenix already provides a session cookie, is because | ||
Phoenix' default session cookies are not persisted, they are | ||
simply signed and potentially encrypted. This means they are | ||
valid indefinitely, unless you change the signing/encryption | ||
salt. | ||
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 | ||
session they deem invalid. | ||
""" | ||
def build_session_token(user) do | ||
token = :crypto.strong_rand_bytes(@rand_size) | ||
{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 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: user in assoc(token, :user), | ||
where: token.inserted_at > ago(@session_validity_in_days, "day"), | ||
select: user | ||
|
||
{:ok, query} | ||
end | ||
|
||
@doc """ | ||
Returns the token struct for the given token value and context. | ||
""" | ||
def by_token_and_context_query(token, context) do | ||
from UserToken, where: [token: ^token, context: ^context] | ||
end | ||
end |
6 changes: 3 additions & 3 deletions
6
apps/configurator/lib/configurator_web/components/layouts/app.html.heex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
apps/configurator/lib/configurator_web/controllers/home_controller.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
defmodule ConfiguratorWeb.HomeController do | ||
use ConfiguratorWeb, :controller | ||
|
||
def home(conn, _params) do | ||
render(conn, :home) | ||
end | ||
end |
5 changes: 5 additions & 0 deletions
5
apps/configurator/lib/configurator_web/controllers/home_html.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
defmodule ConfiguratorWeb.HomeHTML do | ||
use ConfiguratorWeb, :html | ||
|
||
embed_templates "home_html/*" | ||
end |
8 changes: 8 additions & 0 deletions
8
apps/configurator/lib/configurator_web/controllers/home_html/home.html.heex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<.header> | ||
Welcome to Champions of Mirra Configurator | ||
</.header> | ||
|
||
<.list> | ||
<:item title="Character settings"><.link href={~p"/characters"}>Link</.link></:item> | ||
<:item title="Game settings"><.link href={~p"/game_configurations"}>Link</.link></:item> | ||
</.list> |
33 changes: 33 additions & 0 deletions
33
apps/configurator/lib/configurator_web/controllers/user_session_controller.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
defmodule ConfiguratorWeb.UserSessionController do | ||
@moduledoc false | ||
use ConfiguratorWeb, :controller | ||
|
||
alias Configurator.Accounts | ||
alias ConfiguratorWeb.UserAuth | ||
|
||
def create(conn, params) do | ||
create(conn, params, "Welcome back!") | ||
end | ||
|
||
defp create(conn, %{"user" => user_params}, info) do | ||
%{"email" => email, "password" => password} = user_params | ||
|
||
if user = Accounts.get_user_by_email_and_password(email, password) do | ||
conn | ||
|> put_flash(:info, info) | ||
|> 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") | ||
end | ||
end | ||
|
||
def delete(conn, _params) do | ||
conn | ||
|> put_flash(:info, "Logged out successfully.") | ||
|> UserAuth.log_out_user() | ||
end | ||
end |
33 changes: 33 additions & 0 deletions
33
apps/configurator/lib/configurator_web/live/user_login_live.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
defmodule ConfiguratorWeb.UserLoginLive do | ||
use ConfiguratorWeb, :live_view | ||
|
||
def render(assigns) do | ||
~H""" | ||
<div class="mx-auto max-w-sm"> | ||
<.header class="text-center"> | ||
Sign in to account | ||
</.header> | ||
<.simple_form for={@form} id="login_form" action={~p"/users/log_in"} phx-update="ignore"> | ||
<.input field={@form[:email]} type="email" label="Email" required /> | ||
<.input field={@form[:password]} type="password" label="Password" required /> | ||
<:actions> | ||
<.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" /> | ||
</:actions> | ||
<:actions> | ||
<.button phx-disable-with="Signing in..." class="w-full"> | ||
Sign in <span aria-hidden="true">→</span> | ||
</.button> | ||
</:actions> | ||
</.simple_form> | ||
</div> | ||
""" | ||
end | ||
|
||
def mount(_params, _session, socket) do | ||
email = live_flash(socket.assigns.flash, :email) | ||
form = to_form(%{"email" => email}, as: "user") | ||
{:ok, assign(socket, form: form), temporary_assigns: [form: form]} | ||
end | ||
end |
Oops, something went wrong.