Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ defmodule ResourceManager.Identities.Commands.CreateIdentity do
end
end

def execute(%{"username" => _, "password_hash" => _} = params) do
params
|> CreateUser.cast_and_apply()
|> case do
{:ok, %CreateUser{} = input} -> execute(input)
error -> error
end
end

def execute(%{username: _, password_hash: _} = params) do
params
|> CreateUser.cast_and_apply()
Expand Down
25 changes: 24 additions & 1 deletion apps/rest_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,27 @@ curl -X POST http://localhost:4000/api/v1/auth/protocol/openid-connect/logout \

**Response (204)**:

`No content`
`No content`

### Create an user

**Request**:

```sh
curl -X POST http://localhost:4000/admin/v1/users \
-H "Content-Type: application/json" \
-d '{"username":"yashu", "password":"lcpo", "scopes":["6a3a3771-9f56-4254-9497-927e441dacfc" "8a235ba0-a827-4593-92c9-6248bef4fa06"]}'
```

**Response (201)**:

```json
{
"id":"0c5fb5a7-5d86-4b11-b4e3-facf925b3e9d",
"inserted_at":"2020-10-04T13:23:45",
"is_admin":false,
"status":"active",
"update_at":"2020-10-04T13:23:45",
"username":"yashu"
}
```
29 changes: 29 additions & 0 deletions apps/rest_api/lib/controllers/admin/user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule RestAPI.Controller.Admin.User do
@moduledoc false

use RestAPI.Controller, :controller

alias RestAPI.Ports.{Authenticator, ResourceManager}
alias RestAPI.Views.Admin.User

action_fallback RestAPI.Controllers.Fallback

def create(conn, %{"password" => password} = params) do
with true <- ResourceManager.password_allowed?(password),
password_hash <- Authenticator.generate_hash(password, :argon2),
params <-
Map.merge(params, %{"password_hash" => password_hash, "password_algorithm" => "argon2"}),
{:ok, response} <- ResourceManager.create_identity(params) do
conn
|> put_status(:created)
|> put_view(User)
|> render("create.json", response: response)
else
false ->
{:error, 400, %{password: ["password is not strong enough"]}}

{:error, _any} = error ->
error
end
end
end
14 changes: 14 additions & 0 deletions apps/rest_api/lib/controllers/fallback.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ defmodule RestAPI.Controllers.Fallback do
|> render("400.json")
end

def call(conn, {:error, status, response}) when status in [:bad_request, 400] do
conn
|> put_status(status)
|> put_view(Default)
|> render("400.json", response: response)
end

def call(conn, {:error, :unauthorized}) do
conn
|> put_status(:unauthorized)
Expand All @@ -35,6 +42,13 @@ defmodule RestAPI.Controllers.Fallback do
|> render("404.json")
end

def call(conn, {:error, status, response}) when status in [:unprocessable_entity, 422] do
conn
|> put_status(status)
|> put_view(Default)
|> render("422.json", response: response)
end

def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:bad_request)
Expand Down
7 changes: 7 additions & 0 deletions apps/rest_api/lib/ports/authenticator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ defmodule RestAPI.Ports.Authenticator do
@callback sign_out_all_sessions(subject_id :: String.t(), subject_type :: String.t()) ::
{:ok, count :: integer()} | possible_logout_failures()

@doc "Delegates to Authenticator.generate_hash/2"
@callback generate_hash(password :: String.t(), algorithm :: atom()) :: String.t()

@doc "Authenticates the subject using Resource Owner Flow"
@spec sign_in_resource_owner(input :: map()) :: possible_sign_in_responses()
def sign_in_resource_owner(input), do: implementation().sign_in_resource_owner(input)
Expand Down Expand Up @@ -71,6 +74,10 @@ defmodule RestAPI.Ports.Authenticator do
{:ok, count :: integer()} | possible_logout_failures()
def sign_out_all_sessions(sub, type), do: implementation().sign_out_all_sessions(sub, type)

@doc "Generate a hash using the given password and algorithm"
@spec generate_hash(password :: String.t(), algorithm :: atom()) :: String.t()
def generate_hash(password, algorithm), do: implementation().generate_hash(password, algorithm)

defp implementation do
:rest_api
|> Application.get_env(__MODULE__)
Expand Down
29 changes: 29 additions & 0 deletions apps/rest_api/lib/ports/resource_manager.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule RestAPI.Ports.ResourceManager do
@moduledoc """
Port to access Authenticator domain commands.
"""

@typedoc "All possible create_identity responses"
@type possible_create_identity_response ::
{:ok, struct()} | {:error, Ecto.Changeset.t() | :invalid_params}

@doc "Delegates to ResourceManager.create_identity/1"
@callback create_identity(input :: map()) :: possible_create_identity_response()

@doc "Delegates to ResourceManager.password_allowed?/1"
@callback password_allowed?(password :: String.t()) :: boolean()

@doc "Create a new identity with it's credentials"
@spec create_identity(input :: map()) :: possible_create_identity_response()
def create_identity(input), do: implementation().create_identity(input)

@doc "Checks if the given password is strong enough to be used"
@spec password_allowed?(password :: String.t()) :: boolean()
def password_allowed?(password), do: implementation().password_allowed?(password)

defp implementation do
:rest_api
|> Application.get_env(__MODULE__)
|> Keyword.get(:domain)
end
end
6 changes: 6 additions & 0 deletions apps/rest_api/lib/routers/public.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ defmodule RestAPI.Routers.Public do
end
end
end

scope "/admin/v1", RestAPI.Controller.Admin do
pipe_through :authenticated

resources("/users", User, except: [:new])
end
end
16 changes: 16 additions & 0 deletions apps/rest_api/lib/views/admin/user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule RestAPI.Views.Admin.User do
@moduledoc false

use RestAPI.View

def render("create.json", %{response: response}) do
%{
id: response.id,
username: response.username,
status: response.status,
is_admin: response.is_admin,
inserted_at: response.inserted_at,
update_at: response.updated_at
}
end
end
20 changes: 19 additions & 1 deletion apps/rest_api/lib/views/errors/default.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ defmodule RestAPI.Views.Errors.Default do

use RestAPI.View

def render("400.json", %{response: response}) do
%{
status: 400,
error: "bad_request",
detail: "The given params failed in validation",
response: response
}
end

def render("400.json", _assigns) do
%{
status: 400,
Expand Down Expand Up @@ -43,10 +52,19 @@ defmodule RestAPI.Views.Errors.Default do
}
end

def render("422.json", %{response: response}) do
%{
status: 422,
detail: "The given params failed in validation",
response: response,
error: "unprocessable entity"
}
end

def render("changeset.json", %{response: response}) do
%{
status: 400,
detail: "The given params are invalid",
detail: "The given params failed in validation",
response: Ecto.Changeset.traverse_errors(response, &translate_error/1),
error: "bad_request"
}
Expand Down
Loading