diff --git a/lib/chore_tracker/accounts.ex b/lib/chore_tracker/accounts.ex index d40ff20..bd63cff 100644 --- a/lib/chore_tracker/accounts.ex +++ b/lib/chore_tracker/accounts.ex @@ -48,9 +48,15 @@ defmodule ChoreTracker.Accounts do 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) + def get_user_by_login_and_password(login, password) + when is_binary(login) and is_binary(password) do + user = + if String.contains?(login, "@") do + Repo.get_by(User, email: login) + else + Repo.get_by(User, username: login) + end + if User.valid_password?(user, password), do: user end @@ -84,7 +90,7 @@ defmodule ChoreTracker.Accounts do {:error, %Ecto.Changeset{}} """ - def register_user(attrs, opts) do + def register_user(attrs, opts \\ []) do %User{} |> User.registration_changeset(attrs, opts) |> Repo.insert() diff --git a/lib/chore_tracker/accounts/user.ex b/lib/chore_tracker/accounts/user.ex index 8a22969..e75ef69 100644 --- a/lib/chore_tracker/accounts/user.ex +++ b/lib/chore_tracker/accounts/user.ex @@ -3,6 +3,7 @@ defmodule ChoreTracker.Accounts.User do import Ecto.Changeset schema "users" do + field :username, :string field :email, :string field :password, :string, virtual: true, redact: true field :hashed_password, :string, redact: true @@ -38,11 +39,23 @@ defmodule ChoreTracker.Accounts.User do """ def registration_changeset(user, attrs, opts \\ []) do user - |> cast(attrs, [:email, :password, :display_name]) + |> cast(attrs, [:username, :display_name, :email, :password]) + |> validate_username() |> validate_email(opts) |> validate_password(opts) end + defp validate_username(changeset) do + changeset + |> validate_required([:username]) + |> validate_length(:username, min: 3, max: 30) + |> validate_format(:username, ~r/^[a-zA-Z0-9_]+$/, + message: "username can only include letters, numbers and underscores" + ) + |> unsafe_validate_unique(:username, ChoreTracker.Repo) + |> unique_constraint(:username) + end + defp validate_email(changeset, opts) do changeset |> validate_required([:email]) diff --git a/lib/chore_tracker_web/controllers/user_session_controller.ex b/lib/chore_tracker_web/controllers/user_session_controller.ex index dbf54fc..e2f679e 100644 --- a/lib/chore_tracker_web/controllers/user_session_controller.ex +++ b/lib/chore_tracker_web/controllers/user_session_controller.ex @@ -19,9 +19,10 @@ defmodule ChoreTrackerWeb.UserSessionController do end defp create(conn, %{"user" => user_params}, info) do - %{"email" => email, "password" => password} = user_params + %{"password" => password} = user_params + login = user_params["login"] || user_params["username"] || user_params["email"] - if user = Accounts.get_user_by_email_and_password(email, password) do + if user = Accounts.get_user_by_login_and_password(login, password) do conn |> put_flash(:info, info) |> UserAuth.log_in_user(user, user_params) @@ -29,7 +30,7 @@ defmodule ChoreTrackerWeb.UserSessionController do # 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)) + |> put_flash(:login, String.slice(login, 0, 160)) |> redirect(to: ~p"/users/log_in") end end diff --git a/lib/chore_tracker_web/live/user_login_live.ex b/lib/chore_tracker_web/live/user_login_live.ex index 9f36ed5..816e257 100644 --- a/lib/chore_tracker_web/live/user_login_live.ex +++ b/lib/chore_tracker_web/live/user_login_live.ex @@ -16,7 +16,7 @@ defmodule ChoreTrackerWeb.UserLoginLive do <.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[:login]} label="Username or email" required /> <.input field={@form[:password]} type="password" label="Password" required /> <:actions> diff --git a/lib/chore_tracker_web/live/user_registration_live.ex b/lib/chore_tracker_web/live/user_registration_live.ex index 5663e1a..308dfcd 100644 --- a/lib/chore_tracker_web/live/user_registration_live.ex +++ b/lib/chore_tracker_web/live/user_registration_live.ex @@ -31,6 +31,8 @@ defmodule ChoreTrackerWeb.UserRegistrationLive do Oops, something went wrong! Please check the errors below. + <.input field={@form[:username]} label="Username" required /> + <.input field={@form[:display_name]} label="Display name" required /> <.input field={@form[:email]} type="email" label="Email" required /> <.input field={@form[:password]} type="password" label="Password" required /> diff --git a/priv/repo/migrations/20240807220439_create_users_auth_tables.exs b/priv/repo/migrations/20240807220439_create_users_auth_tables.exs index 1437c92..4edf856 100644 --- a/priv/repo/migrations/20240807220439_create_users_auth_tables.exs +++ b/priv/repo/migrations/20240807220439_create_users_auth_tables.exs @@ -5,6 +5,7 @@ defmodule ChoreTracker.Repo.Migrations.CreateUsersAuthTables do execute "CREATE EXTENSION IF NOT EXISTS citext", "" create table(:users) do + add :username, :citext, null: false add :email, :citext, null: false add :hashed_password, :string, null: false add :confirmed_at, :utc_datetime @@ -13,6 +14,7 @@ defmodule ChoreTracker.Repo.Migrations.CreateUsersAuthTables do timestamps(type: :utc_datetime) end + create unique_index(:users, [:username]) create unique_index(:users, [:email]) create table(:users_tokens) do diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index f314aa0..823f452 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -10,35 +10,38 @@ # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. -admin_password = "secret" - -{:ok, admin} = +ondrej = ChoreTracker.Accounts.register_user( %{ + username: "ondrej", email: "ondrej@jednota.party", - password: admin_password, + password: "secret", display_name: "Ondřej" }, disable_password_validation: true ) -ChoreTracker.Accounts.register_user( - %{ - email: "matej@jednota.party", - password: "secret", - display_name: "Matěj" - }, - disable_password_validation: true -) +matej = + ChoreTracker.Accounts.register_user( + %{ + username: "matej", + email: "matej@jednota.party", + password: "secret", + display_name: "Matěj" + }, + disable_password_validation: true + ) -ChoreTracker.Accounts.register_user( - %{ - email: "valentyna@jednota.party", - password: "secret", - display_name: "Valentýna" - }, - disable_password_validation: true -) +valentyna = + ChoreTracker.Accounts.register_user( + %{ + username: "valentyna", + email: "valentyna@jednota.party", + password: "secret", + display_name: "Valentýna" + }, + disable_password_validation: true + ) ChoreTracker.Chores.create_chore(%{ name: "Uklidit kuchyň", @@ -50,7 +53,12 @@ ChoreTracker.Chores.create_chore(%{ - Umýt lednici - Vytřít podlahu """, - starts_at: Date.utc_today() + starts_at: Date.utc_today(), + assignees: [ + ondrej, + matej, + valentyna + ] }) ChoreTracker.Chores.create_chore(%{ @@ -62,7 +70,9 @@ ChoreTracker.Chores.create_chore(%{ - Vyluxovat - Vytřít podlahu """, - starts_at: Date.utc_today() + starts_at: Date.utc_today(), + assignees: [ + ondrej, + matej + ] }) - -IO.puts(["✓ Admin user created\n", " login: ", admin.email, "\n password: ", admin_password])