This commit is contained in:
@@ -61,6 +61,10 @@ config :logger, :console,
|
|||||||
# Use Jason for JSON parsing in Phoenix
|
# Use Jason for JSON parsing in Phoenix
|
||||||
config :phoenix, :json_library, Jason
|
config :phoenix, :json_library, Jason
|
||||||
|
|
||||||
|
config :link_shortener, LinkShortenerWeb.Auth.Guardian,
|
||||||
|
issuer: "link_shortener",
|
||||||
|
secret_key: System.get_env("SECRET_KEY_BASE")
|
||||||
|
|
||||||
# Import environment specific config. This must remain at the bottom
|
# Import environment specific config. This must remain at the bottom
|
||||||
# of this file so it overrides the configuration defined above.
|
# of this file so it overrides the configuration defined above.
|
||||||
import_config "#{config_env()}.exs"
|
import_config "#{config_env()}.exs"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
defmodule LinkShortenerWeb.Auth.ErrorHandler do
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
|
def auth_error(conn, {type, _reason}, _opts) do
|
||||||
|
body = Poison.encode!(%{error: to_string(type)})
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("application/json")
|
||||||
|
|> send_resp(401, body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
defmodule LinkShortenerWeb.Auth.Guardian do
|
||||||
|
use Guardian, otp_app: :link_shortener
|
||||||
|
|
||||||
|
alias LinkShortener.Accounts
|
||||||
|
alias LinkShortener.Accounts.User
|
||||||
|
|
||||||
|
def subject_for_token(user, _claims) do
|
||||||
|
{:ok, to_string(user.id)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_from_claims(%{"sub" => id}) do
|
||||||
|
user = Accounts.get_user!(id)
|
||||||
|
{:ok, user}
|
||||||
|
rescue
|
||||||
|
Ecto.NoResultsError -> {:error, :resource_not_found}
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate(email, password) do
|
||||||
|
with user <- Accounts.get_user_by_email_and_password(email, password) do
|
||||||
|
case user do
|
||||||
|
%User{} -> create_token(user)
|
||||||
|
nil -> {:error, :unauthorized}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_token(user) do
|
||||||
|
{:ok, token, _claims} = encode_and_sign(user)
|
||||||
|
{:ok, user, token}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
defmodule LinkShortenerWeb.Auth.Pipeline do
|
||||||
|
use Guardian.Plug.Pipeline, otp_app: :link_shortener,
|
||||||
|
module: LinkShortenerWeb.Auth.Guardian,
|
||||||
|
error_handler: LinkShortenerWeb.Auth.ErrorHandler
|
||||||
|
|
||||||
|
plug Guardian.Plug.VerifyHeader
|
||||||
|
plug Guardian.Plug.EnsureAuthenticated
|
||||||
|
plug Guardian.Plug.LoadResource
|
||||||
|
end
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
defmodule LinkShortenerWeb.Api.V1.AccountsController do
|
||||||
|
use LinkShortenerWeb, :controller
|
||||||
|
|
||||||
|
alias LinkShortener.Accounts
|
||||||
|
alias LinkShortener.Accounts.User
|
||||||
|
alias LinkShortenerWeb.Auth.Guardian
|
||||||
|
|
||||||
|
action_fallback LinkShortenerWeb.FallbackController
|
||||||
|
|
||||||
|
def sign_up(conn, %{"user" => user_params}) do
|
||||||
|
with {:ok, %User{} = user} <- Accounts.register_user(user_params),
|
||||||
|
{:ok, token, _claims} <- Guardian.encode_and_sign(user) do
|
||||||
|
conn
|
||||||
|
|> put_status(:created)
|
||||||
|
|> render(:user, %{user: user, token: token})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign_in(conn, %{"email" => email, "password" => password}) do
|
||||||
|
with {:ok, user, token} <- Guardian.authenticate(email, password) do
|
||||||
|
conn
|
||||||
|
|> put_status(:created)
|
||||||
|
|> render(:user, %{user: user, token: token})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
defmodule LinkShortenerWeb.Api.V1.AccountsJSON do
|
||||||
|
alias LinkShortener.Links.Link
|
||||||
|
|
||||||
|
def user(%{user: user, token: token}) do
|
||||||
|
%{
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
token: token
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -21,4 +21,11 @@ defmodule LinkShortenerWeb.FallbackController do
|
|||||||
|> put_view(html: LinkShortenerWeb.ErrorHTML, json: LinkShortenerWeb.ErrorJSON)
|
|> put_view(html: LinkShortenerWeb.ErrorHTML, json: LinkShortenerWeb.ErrorJSON)
|
||||||
|> render(:"404")
|
|> render(:"404")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def call(conn, {:error, :unauthorized}) do
|
||||||
|
conn
|
||||||
|
|> put_status(:unauthorized)
|
||||||
|
|> put_view(html: LinkShortenerWeb.ErrorHTML, json: LinkShortenerWeb.ErrorJSON)
|
||||||
|
|> render(:"401")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ defmodule LinkShortenerWeb.Router do
|
|||||||
pipe_through :api
|
pipe_through :api
|
||||||
|
|
||||||
scope "/v1", Api.V1, as: :v1 do
|
scope "/v1", Api.V1, as: :v1 do
|
||||||
|
post "/users/sign_up", AccountsController, :sign_up
|
||||||
|
post "/users/sign_in", AccountsController, :sign_in
|
||||||
|
|
||||||
resources "/links", LinkController
|
resources "/links", LinkController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ defmodule LinkShortener.MixProject do
|
|||||||
{:gettext, "~> 0.20"},
|
{:gettext, "~> 0.20"},
|
||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:dns_cluster, "~> 0.1.1"},
|
{:dns_cluster, "~> 0.1.1"},
|
||||||
{:bandit, "~> 1.5"}
|
{:bandit, "~> 1.5"},
|
||||||
|
{:guardian, "~> 2.3"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,11 @@
|
|||||||
"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"},
|
"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"},
|
||||||
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
|
"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"},
|
"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"]},
|
||||||
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
|
"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"},
|
"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"},
|
||||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||||
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
defmodule LinkShortenerWeb.Api.V1.AccountsControllerTest do
|
||||||
|
use LinkShortenerWeb.ConnCase
|
||||||
|
|
||||||
|
import LinkShortener.AccountsFixtures
|
||||||
|
|
||||||
|
alias LinkShortener.Accounts.User
|
||||||
|
|
||||||
|
@create_attrs %{
|
||||||
|
email: "user@mail.com",
|
||||||
|
password: "some password"
|
||||||
|
}
|
||||||
|
|
||||||
|
@update_attrs %{
|
||||||
|
email: "some updated email",
|
||||||
|
password: "some updated password"
|
||||||
|
}
|
||||||
|
|
||||||
|
@invalid_password_attrs %{
|
||||||
|
email: "user@mail.com",
|
||||||
|
password: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@invalid_attrs %{
|
||||||
|
email: nil,
|
||||||
|
encrypted_password: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
setup %{conn: conn} do
|
||||||
|
{:ok, conn: put_req_header(conn, "accept", "application/json")}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create user with sign up" do
|
||||||
|
test "renders user when data is valid", %{conn: conn} do
|
||||||
|
conn = post(conn, ~p"/api/v1/users/sign_up", user: @create_attrs)
|
||||||
|
assert %{
|
||||||
|
"email" => "user@mail.com",
|
||||||
|
"token" => token
|
||||||
|
} = json_response(conn, 201)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors when data is invalid", %{conn: conn} do
|
||||||
|
conn = post(conn, ~p"/api/v1/users/sign_up", user: @invalid_attrs)
|
||||||
|
assert json_response(conn, 422)["errors"] != %{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "user sign in" do
|
||||||
|
setup [:create_user]
|
||||||
|
|
||||||
|
test "renders user when data is valid", %{conn: conn} do
|
||||||
|
conn = post(conn, ~p"/api/v1/users/sign_in", @create_attrs)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"email" => email,
|
||||||
|
"token" => token,
|
||||||
|
} = json_response(conn, 201)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors when data is invalid", %{conn: conn} do
|
||||||
|
conn = post(conn, ~p"/api/v1/users/sign_in", @invalid_password_attrs)
|
||||||
|
assert %{
|
||||||
|
"errors" => %{"detail" => "Unauthorized"}
|
||||||
|
} = json_response(conn, 401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_user(_) do
|
||||||
|
user = user_fixture(@create_attrs)
|
||||||
|
%{user: user}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,13 @@ defmodule LinkShortener.AccountsFixtures do
|
|||||||
entities via the `LinkShortener.Accounts` context.
|
entities via the `LinkShortener.Accounts` context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias LinkShortener.Accounts
|
||||||
|
alias LinkShortener.Accounts.User
|
||||||
|
alias LinkShortenerWeb.Auth.Guardian
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generate a unique user email.
|
||||||
|
"""
|
||||||
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
|
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
|
||||||
def valid_user_password, do: "hello world!"
|
def valid_user_password, do: "hello world!"
|
||||||
|
|
||||||
@@ -23,6 +30,18 @@ defmodule LinkShortener.AccountsFixtures do
|
|||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_token_fixture(attrs \\ %{}) do
|
||||||
|
user_params = %{
|
||||||
|
email: "user@mail.com",
|
||||||
|
password: "some password"
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, %User{} = user} = Accounts.register_user(user_params)
|
||||||
|
{:ok, token, _claims} = Guardian.encode_and_sign(user)
|
||||||
|
|
||||||
|
token
|
||||||
|
end
|
||||||
|
|
||||||
def extract_user_token(fun) do
|
def extract_user_token(fun) do
|
||||||
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
|
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
|
||||||
[_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
|
[_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
|
||||||
|
|||||||
Reference in New Issue
Block a user