-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
421 additions
and
4 deletions.
There are no files selected for viewing
File renamed without changes.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,3 +25,4 @@ erl_crash.dump | |
# Ignore package tarball (built via "mix hex.build"). | ||
habits-*.tar | ||
|
||
.direnv |
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,32 @@ | ||
defmodule Habits.Users do | ||
import Ecto.Query, warn: false | ||
alias Habits.Repo | ||
|
||
alias Habits.Users.User | ||
|
||
def list_users do | ||
Repo.all(User) | ||
end | ||
|
||
def get_user!(id), do: Repo.get!(User, id) | ||
|
||
def create_user(attrs \\ %{}) do | ||
%User{} | ||
|> User.registration_changeset(attrs) | ||
|> Repo.insert() | ||
end | ||
|
||
def update_user(%User{} = user, attrs) do | ||
user | ||
|> User.registration_changeset(attrs) | ||
|> Repo.update() | ||
end | ||
|
||
def delete_user(%User{} = user) do | ||
Repo.delete(user) | ||
end | ||
|
||
def change_user(%User{} = user, attrs \\ %{}) do | ||
User.registration_changeset(user, attrs) | ||
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,63 @@ | ||
defmodule Habits.Users.User do | ||
use Ecto.Schema | ||
import Ecto.Changeset | ||
|
||
schema "users" do | ||
field :handle, :string | ||
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 | ||
|
||
def registration_changeset(user, attrs) do | ||
user | ||
|> cast(attrs, [:handle, :email, :password]) | ||
|> validate_email() | ||
|> validate_handle() | ||
|> validate_password() | ||
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, Habits.Repo) | ||
|> unique_constraint(:email) | ||
end | ||
|
||
defp validate_handle(changeset) do | ||
changeset | ||
|> validate_required([:handle]) | ||
|> validate_format(:handle, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces") | ||
|> validate_length(:handle, min: 3, max: 20) | ||
|> unsafe_validate_unique(:handle, Habits.Repo) | ||
|> unique_constraint(:handle) | ||
end | ||
|
||
defp validate_password(changeset) do | ||
changeset | ||
|> validate_required([:password]) | ||
|> validate_length(:password, min: 12, max: 72) | ||
|> maybe_hash_password() | ||
end | ||
|
||
defp maybe_hash_password(changeset) do | ||
password = get_change(changeset, :password) | ||
|
||
if 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 | ||
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,25 @@ | ||
defmodule HabitsWeb.ChangesetJSON do | ||
@doc """ | ||
Renders changeset errors. | ||
""" | ||
def error(%{changeset: changeset}) do | ||
# When encoded, the changeset returns its errors | ||
# as a JSON object. So we just pass it forward. | ||
%{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)} | ||
end | ||
|
||
defp translate_error({msg, opts}) do | ||
# You can make use of gettext to translate error messages by | ||
# uncommenting and adjusting the following code: | ||
|
||
# if count = opts[:count] do | ||
# Gettext.dngettext(HabitsWeb.Gettext, "errors", msg, msg, count, opts) | ||
# else | ||
# Gettext.dgettext(HabitsWeb.Gettext, "errors", msg, opts) | ||
# end | ||
|
||
Enum.reduce(opts, msg, fn {key, value}, acc -> | ||
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) | ||
end) | ||
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,24 @@ | ||
defmodule HabitsWeb.FallbackController do | ||
@moduledoc """ | ||
Translates controller action results into valid `Plug.Conn` responses. | ||
See `Phoenix.Controller.action_fallback/1` for more details. | ||
""" | ||
use HabitsWeb, :controller | ||
|
||
# This clause handles errors returned by Ecto's insert/update/delete. | ||
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do | ||
conn | ||
|> put_status(:unprocessable_entity) | ||
|> put_view(json: HabitsWeb.ChangesetJSON) | ||
|> render(:error, changeset: changeset) | ||
end | ||
|
||
# This clause is an example of how to handle resources that cannot be found. | ||
def call(conn, {:error, :not_found}) do | ||
conn | ||
|> put_status(:not_found) | ||
|> put_view(html: HabitsWeb.ErrorHTML, json: HabitsWeb.ErrorJSON) | ||
|> render(:"404") | ||
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,43 @@ | ||
defmodule HabitsWeb.UserController do | ||
use HabitsWeb, :controller | ||
|
||
alias Habits.Users | ||
alias Habits.Users.User | ||
|
||
action_fallback HabitsWeb.FallbackController | ||
|
||
def index(conn, _params) do | ||
users = Users.list_users() | ||
render(conn, :index, users: users) | ||
end | ||
|
||
def create(conn, %{"user" => user_params}) do | ||
with {:ok, %User{} = user} <- Users.create_user(user_params) do | ||
conn | ||
|> put_status(:created) | ||
|> put_resp_header("location", ~p"/api/users/#{user}") | ||
|> render(:show, user: user) | ||
end | ||
end | ||
|
||
def show(conn, %{"id" => id}) do | ||
user = Users.get_user!(id) | ||
render(conn, :show, user: user) | ||
end | ||
|
||
def update(conn, %{"id" => id, "user" => user_params}) do | ||
user = Users.get_user!(id) | ||
|
||
with {:ok, %User{} = user} <- Users.update_user(user, user_params) do | ||
render(conn, :show, user: user) | ||
end | ||
end | ||
|
||
def delete(conn, %{"id" => id}) do | ||
user = Users.get_user!(id) | ||
|
||
with {:ok, %User{}} <- Users.delete_user(user) do | ||
send_resp(conn, :no_content, "") | ||
end | ||
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,27 @@ | ||
defmodule HabitsWeb.UserJSON do | ||
alias Habits.Users.User | ||
|
||
@doc """ | ||
Renders a list of users. | ||
""" | ||
def index(%{users: users}) do | ||
%{data: for(user <- users, do: data(user))} | ||
end | ||
|
||
@doc """ | ||
Renders a single user. | ||
""" | ||
def show(%{user: user}) do | ||
%{data: data(user)} | ||
end | ||
|
||
defp data(%User{} = user) do | ||
%{ | ||
id: user.id, | ||
handle: user.handle, | ||
email: user.email, | ||
hashed_password: user.hashed_password, | ||
confirmed_at: user.confirmed_at | ||
} | ||
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
defmodule Habits.Repo.Migrations.CreateUsers do | ||
use Ecto.Migration | ||
|
||
def change do | ||
create table(:users) do | ||
add :handle, :string | ||
add :email, :string | ||
add :hashed_password, :string | ||
add :confirmed_at, :naive_datetime | ||
|
||
timestamps(type: :utc_datetime) | ||
end | ||
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,65 @@ | ||
defmodule Habits.UsersTest do | ||
use Habits.DataCase | ||
|
||
alias Habits.Users | ||
|
||
describe "users" do | ||
alias Habits.Users.User | ||
|
||
import Habits.UsersFixtures | ||
|
||
@invalid_attrs %{handle: nil, email: nil, hashed_password: nil, confirmed_at: nil} | ||
|
||
test "list_users/0 returns all users" do | ||
user = user_fixture() | ||
assert Users.list_users() == [user] | ||
end | ||
|
||
test "get_user!/1 returns the user with given id" do | ||
user = user_fixture() | ||
assert Users.get_user!(user.id) == user | ||
end | ||
|
||
test "create_user/1 with valid data creates a user" do | ||
valid_attrs = %{handle: "some handle", email: "some email", hashed_password: "some hashed_password", confirmed_at: ~N[2024-07-05 19:47:00]} | ||
|
||
assert {:ok, %User{} = user} = Users.create_user(valid_attrs) | ||
assert user.handle == "some handle" | ||
assert user.email == "some email" | ||
assert user.hashed_password == "some hashed_password" | ||
assert user.confirmed_at == ~N[2024-07-05 19:47:00] | ||
end | ||
|
||
test "create_user/1 with invalid data returns error changeset" do | ||
assert {:error, %Ecto.Changeset{}} = Users.create_user(@invalid_attrs) | ||
end | ||
|
||
test "update_user/2 with valid data updates the user" do | ||
user = user_fixture() | ||
update_attrs = %{handle: "some updated handle", email: "some updated email", hashed_password: "some updated hashed_password", confirmed_at: ~N[2024-07-06 19:47:00]} | ||
|
||
assert {:ok, %User{} = user} = Users.update_user(user, update_attrs) | ||
assert user.handle == "some updated handle" | ||
assert user.email == "some updated email" | ||
assert user.hashed_password == "some updated hashed_password" | ||
assert user.confirmed_at == ~N[2024-07-06 19:47:00] | ||
end | ||
|
||
test "update_user/2 with invalid data returns error changeset" do | ||
user = user_fixture() | ||
assert {:error, %Ecto.Changeset{}} = Users.update_user(user, @invalid_attrs) | ||
assert user == Users.get_user!(user.id) | ||
end | ||
|
||
test "delete_user/1 deletes the user" do | ||
user = user_fixture() | ||
assert {:ok, %User{}} = Users.delete_user(user) | ||
assert_raise Ecto.NoResultsError, fn -> Users.get_user!(user.id) end | ||
end | ||
|
||
test "change_user/1 returns a user changeset" do | ||
user = user_fixture() | ||
assert %Ecto.Changeset{} = Users.change_user(user) | ||
end | ||
end | ||
end |
Oops, something went wrong.