Skip to content

Commit

Permalink
Feat/users/post user endpoint (#6)
Browse files Browse the repository at this point in the history
* feat(Users): POST user endpoint

* feat(specs): Created specs schemas for POST user input and response
  • Loading branch information
Gabrielparizet authored Oct 17, 2024
1 parent a16cc9e commit b93aba0
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 49 deletions.
3 changes: 2 additions & 1 deletion lib/book_my_gigs/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ defmodule BookMyGigs.Accounts do

defstruct [:id, :email, :password]

@type id :: String.t()
@type t :: %__MODULE__{
id: String.t(),
email: String.t(),
password: String.t(),
password: String.t()
}
end

Expand Down
16 changes: 12 additions & 4 deletions lib/book_my_gigs/accounts/Storage/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ defmodule BookMyGigs.Accounts.Storage.Account do
account
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_format(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/,
message: "must have the @ sign and no spaces"
)
|> validate_length(:email, max: 160)
|> validate_length(:password, min: 8)
|> validate_format(:password, ~r/[a-z]/, message: "must include at least one lowercase letter")
|> validate_format(:password, ~r/[A-Z]/, message: "must include at least one uppercase letter")
|> validate_format(:password, ~r/[a-z]/,
message: "must include at least one lowercase letter"
)
|> validate_format(:password, ~r/[A-Z]/,
message: "must include at least one uppercase letter"
)
|> validate_format(:password, ~r/[0-9]/, message: "must include at least one number")
|> validate_format(:password, ~r/[?!.,@*€$\-_#:]/, message: "must include at least one special character (?!.,@*€$-_#:)")
|> validate_format(:password, ~r/[?!.,@*€$\-_#:]/,
message: "must include at least one special character (?!.,@*€$-_#:)"
)
|> unique_constraint(:email)
end
end
35 changes: 35 additions & 0 deletions lib/book_my_gigs/users.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule BookMyGigs.Users do
@moduledoc """
The users context
"""

alias BookMyGigs.Accounts
alias BookMyGigs.Users.Storage

defmodule User do
@moduledoc """
Module defining the context struct for a user.
"""

@derive Jason.Encoder

defstruct [:id, :account_id, :username, :first_name, :last_name, :birthday]

@type t :: %__MODULE__{
id: String.t(),
account_id: Accounts.Account.id(),
username: String.t(),
first_name: String.t(),
last_name: String.t(),
birthday: Date.t()
}
end

def create_user(%{"user" => user_params}, account_id) do
Storage.create_user(user_params, account_id)
end

def to_context_struct(%Storage.User{} = index_db) do
struct(User, Map.from_struct(index_db))
end
end
37 changes: 37 additions & 0 deletions lib/book_my_gigs/users/storage.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule BookMyGigs.Users.Storage do
@moduledoc """
Module providing functionalities to interact with the users table.
"""

alias BookMyGigs.Accounts.Storage.Account
alias BookMyGigs.Users
alias BookMyGigs.Users.Storage
alias BookMyGigs.Utils
alias BookMyGigs.Repo

def create_user(user_params, account_id) do
params = %{
account_id: account_id,
username: user_params["username"],
first_name: user_params["first_name"],
last_name: user_params["last_name"],
birthday: Utils.DateUtils.parse_date(user_params["birthday"])
}

case Repo.get(Account, account_id) do
nil ->
{:error, :account_not_found}

_account ->
changeset = Storage.User.changeset(%Storage.User{}, params)

case Repo.insert(changeset) do
{:ok, user} ->
Users.to_context_struct(user)

{:error, changeset} ->
{:error, changeset}
end
end
end
end
19 changes: 6 additions & 13 deletions lib/book_my_gigs/users/storage/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ defmodule BookMyGigs.Users.Storage.User do
import Ecto.Changeset

alias BookMyGigs.Accounts.Storage.Account
alias BookMyGigs.Utils.DateUtils

@schema_prefix "public"
@primary_key {:id, Ecto.UUID, autogenerate: :true}
@primary_key {:id, Ecto.UUID, autogenerate: true}
schema "users" do
field(:username, :string)
field(:first_name, :string)
Expand All @@ -27,24 +26,18 @@ defmodule BookMyGigs.Users.Storage.User do
|> cast(attrs, [:username, :first_name, :last_name, :birthday, :account_id])
|> validate_required([:username, :account_id])
|> validate_length(:username, min: 6, max: 20)
|> validate_format(:birthday, ~r/^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[012])\/\d{4}$/, message: "must be in the format DD/MM/YYYY")
|> validate_birthday()
|> unique_constraint(:username)
|> unique_constraint(:account_id)
end

defp validate_birthday(changeset) do
with birthday_string when not is_nil(birthday_string) <- get_field(changeset, :birthday),
{:ok, date} <- DateUtils.parse_date(birthday_string),
true <- Date.compare(date, Date.utc_today()) == :lt do
put_change(changeset, :birthday, date)
birthday = get_change(changeset, :birthday)

if Date.compare(birthday, Date.utc_today()) == :lt do
changeset
else
nil ->
changeset
{:error, _} ->
add_error(changeset, :birthday, "is invalid, must be in the format 'DD/MM/YYYY'")
false ->
add_error(changeset, :birthday, "must be in the past")
add_error(changeset, :birthday, "must be in the past")
end
end
end
11 changes: 3 additions & 8 deletions lib/book_my_gigs/utils/date_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@ defmodule BookMyGigs.Utils.DateUtils do
"""

def parse_date(date_string) do
with [day, month, year] <- String.split(date_string, "/"),
{day, ""} <- Integer.parse(day),
{month, ""} <- Integer.parse(month),
{year, ""} <- Integer.parse(year) do
Date.new(year, month, day)
else
_ -> :error
end
[day, month, year] = String.split(date_string, "/")
[year, month, day] = Enum.map([year, month, day], &String.to_integer/1)
Date.new!(year, month, day)
end
end
42 changes: 21 additions & 21 deletions lib/book_my_gigs_web/accounts/accounts_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ defmodule BookMyGigsWeb.AccountsController do
ok: "Account successfully created"
)

def create(conn, params) do
case Accounts.create_account(params) do
{:ok, account} ->
{:ok, token, _claims} = BookMyGigs.Guardian.encode_and_sign(account)

response = %{
account: account,
token: token
}

conn
|> put_resp_content_type("application/json")
|> send_resp(201, Jason.encode!(response))

{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(400, Jason.encode!(%{error: reason}))
end
end

operation(:get_account_by_id,
summary: "Get account by id",
parameters: [
Expand Down Expand Up @@ -67,27 +88,6 @@ defmodule BookMyGigsWeb.AccountsController do
|> send_resp(200, account)
end

def create(conn, params) do
case Accounts.create_account(params) do
{:ok, account} ->
{:ok, token, _claims} = BookMyGigs.Guardian.encode_and_sign(account)

response = %{
account: account,
token: token
}

conn
|> put_resp_content_type("application/json")
|> send_resp(201, Jason.encode!(response))

{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(400, Jason.encode!(%{error: reason}))
end
end

operation(:update,
summary: "Update an account",
parameters: [
Expand Down
3 changes: 2 additions & 1 deletion lib/book_my_gigs_web/accounts/schemas/Account_response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ defmodule BookMyGigsWeb.Accounts.Schemas.AccountResponse do
type: :object,
properties: %{
email: %Schema{
type: :string
type: :string,
format: :email
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions lib/book_my_gigs_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ defmodule BookMyGigsWeb.Router do
get "/accounts/:id", AccountsController, :get_account_by_id
put "/accounts/:id", AccountsController, :update
delete "/accounts/:id", AccountsController, :delete

# USERS ROUTES
post "/users/", UsersController, :create
end

# Other scopes may use custom stacks.
Expand Down
42 changes: 42 additions & 0 deletions lib/book_my_gigs_web/users/schemas/create_user_input.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule BookMyGigsWeb.Users.Schemas.CreateUserInput do
@moduledoc """
Specs describing the response when creating an user.
"""

alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(%{
title: "Create User Input",
description: "Valid input values to create an user",
type: :object,
properties: %{
user: %Schema{
type: :object,
properties: %{
username: %Schema{
type: :string
},
first_name: %Schema{
type: :string
},
last_name: %Schema{
type: :string
},
birthday: %Schema{
type: :string
}
}
}
},
example: %{
"user" => %{
"username" => "Gabdu20",
"first_name" => "Gabriel",
"last_name" => "Parizet",
"birthday" => "04/07/1994"
}
}
})
end
46 changes: 46 additions & 0 deletions lib/book_my_gigs_web/users/schemas/user_response.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule BookMyGigsWeb.Users.Schemas.UserResponse do
@moduledoc """
Specs describing the response when creating a user.
"""

alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(%{
title: "User response",
description: "Schema describing the response when creating a user",
type: :object,
properties: %{
id: %Schema{
type: :string,
format: :uuid
},
account_id: %Schema{
type: :string,
format: :uuid
},
username: %Schema{
type: :string
},
first_name: %Schema{
type: :string
},
last_name: %Schema{
type: :string
},
birthday: %Schema{
type: :string,
format: :date
}
},
example: %{
"id" => "b57ed6b1-02a7-4cf3-89bb-34ac1f424b79",
"account_id" => "1e531b65-44dc-44d8-a772-0f2353133444",
"username" => "MyUsername",
"first_name" => "Gabriel",
"last_name" => "Parizet",
"birthday" => "1994-04-20"
}
})
end
42 changes: 42 additions & 0 deletions lib/book_my_gigs_web/users/users_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule BookMyGigsWeb.UsersController do
@moduledoc """
The Users Controller
"""
use BookMyGigsWeb, :controller
use OpenApiSpex.ControllerSpecs

alias BookMyGigs.Users
alias BookMyGigsWeb.Users.Schemas
alias OpenApiSpex.Schema

operation(:create,
summary: "Create an user",
parameters: [
account_id: [
in: :path,
description: "User id",
schema: %Schema{type: :string, format: :uuid},
example: "61492a85-3946-4b62-8887-2952af807c26"
]
],
request_body: {"Create user input", "application/json", Schemas.CreateUserInput},
responses: [
ok: {"User response", "application/json", Schemas.UserResponse},
bad_request: "Invalid input values"
],
ok: "User successfully created"
)

def create(conn, params) do
account_id = conn.private[:guardian_default_resource].id

user =
params
|> Users.create_user(account_id)
|> Jason.encode!()

conn
|> put_resp_content_type("application/json")
|> send_resp(200, user)
end
end
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
"guardian": {:hex, :guardian, "2.3.2", "78003504b987f2b189d76ccf9496ceaa6a454bb2763627702233f31eb7212881", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b189ff38cd46a22a8a824866a6867ca8722942347f13c33f7d23126af8821b52"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
Expand Down

0 comments on commit b93aba0

Please sign in to comment.