Users can access their items via API. Authentication via API token. No public access to items.
This commit is contained in:
@ -281,6 +281,32 @@ defmodule GenericRestServer.Accounts do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a new api token for a user.
|
||||||
|
|
||||||
|
The token returned must be saved somewhere safe.
|
||||||
|
This token cannot be recovered from the database.
|
||||||
|
"""
|
||||||
|
def create_user_api_token(user) do
|
||||||
|
{encoded_token, user_token} = UserToken.build_email_token(user, "api-token")
|
||||||
|
Repo.insert!(user_token)
|
||||||
|
encoded_token
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Fetches the user by API token.
|
||||||
|
"""
|
||||||
|
def fetch_user_by_api_token(token) do
|
||||||
|
with {:ok, query} <- UserToken.verify_api_token_query(token),
|
||||||
|
%User{} = user <- Repo.one(query) do
|
||||||
|
{:ok, user}
|
||||||
|
else
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
## Token helper
|
## Token helper
|
||||||
|
|
||||||
defp update_user_and_delete_all_tokens(changeset) do
|
defp update_user_and_delete_all_tokens(changeset) do
|
||||||
|
|||||||
@ -11,6 +11,7 @@ defmodule GenericRestServer.Accounts.UserToken do
|
|||||||
@magic_link_validity_in_minutes 15
|
@magic_link_validity_in_minutes 15
|
||||||
@change_email_validity_in_days 7
|
@change_email_validity_in_days 7
|
||||||
@session_validity_in_days 14
|
@session_validity_in_days 14
|
||||||
|
@api_token_validity_in_days 30
|
||||||
|
|
||||||
@primary_key {:id, :binary_id, autogenerate: true}
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
@foreign_key_type :binary_id
|
@foreign_key_type :binary_id
|
||||||
@ -155,4 +156,35 @@ defmodule GenericRestServer.Accounts.UserToken do
|
|||||||
defp by_token_and_context_query(token, context) do
|
defp by_token_and_context_query(token, context) do
|
||||||
from UserToken, where: [token: ^token, context: ^context]
|
from UserToken, where: [token: ^token, context: ^context]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if the API token is valid and returns its underlying lookup query.
|
||||||
|
|
||||||
|
The query returns the user found by the token, if any.
|
||||||
|
|
||||||
|
The given token is valid if it matches its hashed counterpart in the
|
||||||
|
database and the user email has not changed. This function also checks
|
||||||
|
if the token is being used within 365 days.
|
||||||
|
"""
|
||||||
|
def verify_api_token_query(token) do
|
||||||
|
case Base.url_decode64(token, padding: false) do
|
||||||
|
{:ok, decoded_token} ->
|
||||||
|
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
|
||||||
|
|
||||||
|
query =
|
||||||
|
from token in by_token_and_context_query(hashed_token, "api-token"),
|
||||||
|
join: user in assoc(token, :user),
|
||||||
|
where:
|
||||||
|
token.inserted_at > ago(^@api_token_validity_in_days, "day") and
|
||||||
|
token.sent_to == user.email,
|
||||||
|
select: user
|
||||||
|
|
||||||
|
{:ok, query}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
:error
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
defmodule GenericRestServerWeb.UserTokenController do
|
||||||
|
use GenericRestServerWeb, :controller
|
||||||
|
|
||||||
|
alias GenericRestServer.Accounts
|
||||||
|
alias GenericRestServer.Accounts.User
|
||||||
|
|
||||||
|
action_fallback GenericRestServerWeb.FallbackController
|
||||||
|
|
||||||
|
def log_in(conn, %{"user" => user_params}) do
|
||||||
|
case Accounts.get_user_by_email_and_password(user_params["email"], user_params["password"]) do
|
||||||
|
%User{} = user ->
|
||||||
|
create_token(conn, user)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:forbidden)
|
||||||
|
|> render(:error, %{error: "No access for you!"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_token(conn, user) do
|
||||||
|
encoded_token = Accounts.create_user_api_token(user)
|
||||||
|
|
||||||
|
updated_user = Map.put(user, :token, encoded_token)
|
||||||
|
|
||||||
|
render(conn, :token, user: updated_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
17
lib/generic_rest_server_web/controllers/user_token_json.ex
Normal file
17
lib/generic_rest_server_web/controllers/user_token_json.ex
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
defmodule GenericRestServerWeb.UserTokenJSON do
|
||||||
|
def token(%{user: user}) do
|
||||||
|
%{
|
||||||
|
data: %{
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
token: user.token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(%{error: error}) do
|
||||||
|
%{
|
||||||
|
error: error
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -17,16 +17,28 @@ defmodule GenericRestServerWeb.Router do
|
|||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
pipeline :protected_api do
|
||||||
|
plug :accepts, ["json"]
|
||||||
|
plug :fetch_current_scope_for_api_user
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", GenericRestServerWeb do
|
scope "/", GenericRestServerWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/", PageController, :home
|
get "/", PageController, :home
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# public API
|
||||||
scope "/api", GenericRestServerWeb do
|
scope "/api", GenericRestServerWeb do
|
||||||
pipe_through :api
|
pipe_through :api
|
||||||
|
|
||||||
|
post "/log_in", UserTokenController, :log_in
|
||||||
|
end
|
||||||
|
|
||||||
|
# protected API
|
||||||
|
scope "/api", GenericRestServerWeb do
|
||||||
|
pipe_through :protected_api
|
||||||
|
|
||||||
resources "/items", ItemController, except: [:new, :edit]
|
resources "/items", ItemController, except: [:new, :edit]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -284,4 +284,20 @@ defmodule GenericRestServerWeb.UserAuth do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_store_return_to(conn), do: conn
|
defp maybe_store_return_to(conn), do: conn
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
def fetch_current_scope_for_api_user(conn, _opts) do
|
||||||
|
with [<<bearer::binary-size(6), " ", token::binary>>] <-
|
||||||
|
get_req_header(conn, "authorization"),
|
||||||
|
true <- String.downcase(bearer) == "bearer",
|
||||||
|
{:ok, user} <- Accounts.fetch_user_by_api_token(token) do
|
||||||
|
assign(conn, :current_scope, Scope.for_user(user))
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> send_resp(:unauthorized, "No access for you")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -394,4 +394,15 @@ defmodule GenericRestServer.AccountsTest do
|
|||||||
refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
|
refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
describe "create_user_api_token/1 and fetch_user_by_api_token/1" do
|
||||||
|
test "creates and fetches by token" do
|
||||||
|
user = user_fixture()
|
||||||
|
token = Accounts.create_user_api_token(user)
|
||||||
|
assert Accounts.fetch_user_by_api_token(token) == {:ok, user}
|
||||||
|
assert Accounts.fetch_user_by_api_token("invalid") == :error
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user