Skip to content

Commit

Permalink
add users
Browse files Browse the repository at this point in the history
  • Loading branch information
ksevelyar committed Jul 6, 2024
1 parent 6d4a043 commit 65e5f02
Show file tree
Hide file tree
Showing 15 changed files with 421 additions and 4 deletions.
File renamed without changes.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}
strategy:
matrix:
otp: ['24']
elixir: ['1.13.1']
otp: ['27']
elixir: ['1.17.1']
steps:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
Expand All @@ -23,5 +23,5 @@ jobs:
elixir-version: ${{matrix.elixir}}
- run: mix deps.get
- run: mix test
env:
env:
FRONT: http://localhost:8080
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ erl_crash.dump
# Ignore package tarball (built via "mix hex.build").
habits-*.tar

.direnv
32 changes: 32 additions & 0 deletions lib/habits/users.ex
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
63 changes: 63 additions & 0 deletions lib/habits/users/user.ex
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
25 changes: 25 additions & 0 deletions lib/habits_web/controllers/changeset_json.ex
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
24 changes: 24 additions & 0 deletions lib/habits_web/controllers/fallback_controller.ex
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
43 changes: 43 additions & 0 deletions lib/habits_web/controllers/user_controller.ex
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
27 changes: 27 additions & 0 deletions lib/habits_web/controllers/user_json.ex
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
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Habits.MixProject do
[
app: :habits,
version: "0.1.0",
elixir: "~> 1.14",
elixir: "~> 1.17",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
Expand All @@ -32,6 +32,7 @@ defmodule Habits.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
{:bcrypt_elixir, "~> 3.1"},
{:phoenix, "~> 1.7.14"},
{:phoenix_ecto, "~> 4.5"},
{:ecto_sql, "~> 3.10"},
Expand Down
3 changes: 3 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
%{
"bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
"ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"},
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
"expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
Expand Down
14 changes: 14 additions & 0 deletions priv/repo/migrations/20240706194718_create_users.exs
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
65 changes: 65 additions & 0 deletions test/habits/users_test.exs
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

Check failure on line 13 in test/habits/users_test.exs

View workflow job for this annotation

GitHub Actions / OTP 27 / Elixir 1.17.1

test users list_users/0 returns all users (Habits.UsersTest)
user = user_fixture()
assert Users.list_users() == [user]
end

test "get_user!/1 returns the user with given id" do

Check failure on line 18 in test/habits/users_test.exs

View workflow job for this annotation

GitHub Actions / OTP 27 / Elixir 1.17.1

test users get_user!/1 returns the user with given id (Habits.UsersTest)
user = user_fixture()
assert Users.get_user!(user.id) == user
end

test "create_user/1 with valid data creates a user" do

Check failure on line 23 in test/habits/users_test.exs

View workflow job for this annotation

GitHub Actions / OTP 27 / Elixir 1.17.1

test users create_user/1 with valid data creates a user (Habits.UsersTest)
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

Check failure on line 37 in test/habits/users_test.exs

View workflow job for this annotation

GitHub Actions / OTP 27 / Elixir 1.17.1

test users update_user/2 with valid data updates the user (Habits.UsersTest)
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

Check failure on line 48 in test/habits/users_test.exs

View workflow job for this annotation

GitHub Actions / OTP 27 / Elixir 1.17.1

test users update_user/2 with invalid data returns error changeset (Habits.UsersTest)
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

Check failure on line 54 in test/habits/users_test.exs

View workflow job for this annotation

GitHub Actions / OTP 27 / Elixir 1.17.1

test users delete_user/1 deletes the user (Habits.UsersTest)
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

Check failure on line 60 in test/habits/users_test.exs

View workflow job for this annotation

GitHub Actions / OTP 27 / Elixir 1.17.1

test users change_user/1 returns a user changeset (Habits.UsersTest)
user = user_fixture()
assert %Ecto.Changeset{} = Users.change_user(user)
end
end
end
Loading

0 comments on commit 65e5f02

Please sign in to comment.