diff --git a/apps/ex_fleet_yards/lib/ex_fleet_yards/repo/account/oauth_client.ex b/apps/ex_fleet_yards/lib/ex_fleet_yards/repo/account/oauth_client.ex index 1b54780a..b3782422 100644 --- a/apps/ex_fleet_yards/lib/ex_fleet_yards/repo/account/oauth_client.ex +++ b/apps/ex_fleet_yards/lib/ex_fleet_yards/repo/account/oauth_client.ex @@ -20,7 +20,8 @@ defmodule ExFleetYards.Repo.Account.OauthClient do def create(user, %Boruta.Ecto.Client{id: id}), do: create(user, id) def create(%User{id: user_id}, client), do: create(user_id, client) - def create(user_id, %Ecto.Changeset{} = client_changeset) when is_binary(user_id) do + def create(user_id, %Ecto.Changeset{} = client_changeset) + when is_binary(user_id) or is_nil(user_id) do if client_changeset.valid? do Repo.transaction(fn -> with {:ok, client} <- Repo.insert(client_changeset), @@ -101,6 +102,7 @@ defmodule ExFleetYards.Repo.Account.OauthClient do id_token_signing_alg: "RS256", pkce: false } + def default_args, do: @default_args defp transform_client_args(args, true) do args = diff --git a/apps/ex_fleet_yards/lib/ex_fleet_yards/schemas.ex b/apps/ex_fleet_yards/lib/ex_fleet_yards/schemas.ex index deb94c97..782f186c 100644 --- a/apps/ex_fleet_yards/lib/ex_fleet_yards/schemas.ex +++ b/apps/ex_fleet_yards/lib/ex_fleet_yards/schemas.ex @@ -20,4 +20,91 @@ defmodule ExFleetYards.Schemas do example: %{count: 3, offset: 1, limit: 25, total: 4} }) end + + defmodule Result do + @moduledoc false + require OpenApiSpex + + @common_args + + OpenApiSpex.schema(%{ + description: "Generic result", + type: :object, + properties: %{ + code: %Schema{type: :string}, + message: %Schema{type: :string} + }, + required: [:code, :message], + example: %{code: "ok", message: "Operation Successfull"} + }) + end + + # defmacro result(name, description, properties) do + # properties = Macro.expand_once(properties, __CALLER__) + # |> Map.put(:code, %Schema{type: :string}) + # |> Map.put(:message, %Schema{type: :string}) + # |> IO.inspect() + + # quote do + # defmodule unquote(name) do + # @moduledoc unquote(description) + # require OpenApiSpex + + # OpenApiSpex.schema(%{ + # description: unquote(description), + # type: :object, + # properties: unquote(Macro.escape(properties)), + # required: [:code, :message], + # example: %{code: "ok", message: "Operation Successfull"} + # }) + # end + # end + # end + defmacro result(name, description, properties, required) when is_list(required) do + {properties_expanded, _} = + properties + |> Macro.expand_once(__CALLER__) + |> Macro.to_string() + |> Code.eval_string() + + properties_with_defaults = + Map.put_new(properties_expanded, :code, %Schema{type: :string}) + |> Map.put_new(:message, %Schema{type: :string}) + + required = Enum.uniq(required ++ [:code, :message]) + + quote do + defmodule unquote(name) do + @moduledoc unquote(description) + require OpenApiSpex + + OpenApiSpex.schema(%{ + description: unquote(description), + type: :object, + properties: unquote(Macro.escape(properties_with_defaults)), + required: unquote(required), + example: %{code: "ok", message: "Operation Successful"} + }) + end + end + end + + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end + + def controller do + quote do + use OpenApiSpex.ControllerSpecs + alias unquote(__MODULE__).Result + end + end + + def schema do + quote do + alias OpenApiSpex.Schema + require unquote(__MODULE__) + import unquote(__MODULE__), only: [result: 4] + end + end end diff --git a/apps/ex_fleet_yards/priv/repo/migrations/20230730111440_no_user_for_oauth_client.exs b/apps/ex_fleet_yards/priv/repo/migrations/20230730111440_no_user_for_oauth_client.exs new file mode 100644 index 00000000..ae3d0c29 --- /dev/null +++ b/apps/ex_fleet_yards/priv/repo/migrations/20230730111440_no_user_for_oauth_client.exs @@ -0,0 +1,14 @@ +defmodule ExFleetYards.Repo.Migrations.NoUserForOauthClient do + use Ecto.Migration + + def up do + execute "ALTER TABLE oauth_user_clients DROP CONSTRAINT oauth_user_clients_user_id_fkey" + + alter table(:oauth_user_clients) do + modify :user_id, references(:users, type: :uuid, on_delete: :delete_all), null: true + end + end + + def down do + end +end diff --git a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth.ex b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth.ex index 8d4e17b5..ddb2f8af 100644 --- a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth.ex +++ b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth.ex @@ -34,6 +34,27 @@ defmodule ExFleetYardsAuth do end end + def controller_api do + quote do + @moduledoc "Controller used for Auth" + use Phoenix.Controller, + formats: [:html, :json], + layouts: [html: ExFleetYardsAuth.Layouts] + + use ExFleetYards.Schemas, :controller + + import Plug.Conn + + alias ExFleetYards.Repo + + alias ExFleetYardsAuth.Router.Helpers, as: Routes + + unquote(verified_routes()) + + import ExFleetYards.Plugs.ApiAuthorization, only: [authorize: 2] + end + end + def live_view do quote do use Phoenix.LiveView, diff --git a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/api_spec.ex b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/api_spec.ex new file mode 100644 index 00000000..12418b94 --- /dev/null +++ b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/api_spec.ex @@ -0,0 +1,64 @@ +defmodule ExFleetYardsAuth.ApiSpec do + @moduledoc """ + OpenApi Spec definition root + """ + use ExFleetYardsAuth, :verified_routes + + alias OpenApiSpex.{ + Components, + Info, + OpenApi, + Paths, + Server, + SecurityScheme, + OAuthFlows, + OAuthFlow + } + + alias ExFleetYardsAuth.{Endpoint, Router} + @behaviour OpenApi + + @impl OpenApi + def spec do + %OpenApi{ + servers: [ + Server.from_endpoint(Endpoint) + ], + info: %Info{ + title: "Fleetyards", + version: ExFleetYards.Version.version() + }, + paths: Paths.from_router(Router), + components: %Components{ + securitySchemes: %{ + "authorization" => %SecurityScheme{ + type: "oauth2", + scheme: "bearer", + in: "header", + flows: %OAuthFlows{ + authorizationCode: %OAuthFlow{ + authorizationUrl: Endpoint.url() <> ~p"/oauth/authorize", + tokenUrl: Endpoint.url() <> ~p"/oauth/token", + scopes: scope_list() + }, + implicit: %OAuthFlow{ + authorizationUrl: Endpoint.url() <> ~p"/oauth/authorize", + scopes: scope_list() + } + } + } + } + } + } + |> OpenApiSpex.resolve_schema_modules() + end + + defp scope_list do + ExFleetYards.Scopes.scope_list() + |> Enum.map(fn + {scope, description} -> {to_string(scope), description} + {scope, description, _} -> {to_string(scope), description} + end) + |> Enum.into(%{}) + end +end diff --git a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/client_controller.ex b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/client_controller.ex index 234b1705..120843da 100644 --- a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/client_controller.ex +++ b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/client_controller.ex @@ -2,15 +2,23 @@ defmodule ExFleetYardsAuth.Api.ClientController do @moduledoc """ Boruta Client controller """ - use ExFleetYardsAuth, :controller + use ExFleetYardsAuth, :controller_api require Logger - import ExFleetYards.Plugs.ApiAuthorization, only: [authorize: 2] alias ExFleetYards.Repo alias ExFleetYards.Repo.Account.User alias ExFleetYards.Repo.Account.OauthClient + alias ExFleetYardsAuth.Api.ClientSchema plug(:authorize, ["user:security"]) + security [%{"authorization" => ["user:security"]}] + tags ["user", "security"] + + operation :index, + summary: "Return clients owned by user", + responses: [ + ok: {"ClientList", "application/json", ClientSchema.ClientList} + ] def index(conn, _params) do user = @@ -21,6 +29,19 @@ defmodule ExFleetYardsAuth.Api.ClientController do |> render("index.json", clients: user.oauth_clients) end + operation :get, + summary: "Get specific client", + parameters: [ + id: [ + in: :path, + description: "Id of client", + type: %OpenApiSpex.Schema{type: :string, format: :uuid} + ] + ], + responses: [ + ok: {"Client", "application/json", ClientSchema.Client} + ] + def get(conn, %{"id" => id}) do user = conn.assigns[:current_user] @@ -32,6 +53,13 @@ defmodule ExFleetYardsAuth.Api.ClientController do |> render("client.json", client: client) end + operation :post, + summary: "Create client", + request_body: {"Client", "application/json", ClientSchema.Client}, + responses: [ + created: {"Client", "application/json", ClientSchema.Client} + ] + def post(conn, params) do user = conn.assigns[:current_user] @@ -50,6 +78,20 @@ defmodule ExFleetYardsAuth.Api.ClientController do end end + operation :patch, + summary: "Update client", + parameters: [ + id: [ + in: :path, + description: "Id of client", + type: %OpenApiSpex.Schema{type: :string, format: :uuid} + ] + ], + request_body: {"Client", "application/json", ClientSchema.Client}, + responses: [ + ok: {"Client", "application/json", ClientSchema.Client} + ] + def patch(conn, %{"id" => id} = params) do user = conn.assigns[:current_user] @@ -68,6 +110,19 @@ defmodule ExFleetYardsAuth.Api.ClientController do end end + operation :delete, + summary: "Delete a client", + parameters: [ + id: [ + in: :path, + description: "Id of client", + type: %OpenApiSpex.Schema{type: :string, format: :uuid} + ] + ], + responses: [ + ok: {"ClientDelete", "application/json", ClientSchema.ClientDelete} + ] + def delete(conn, %{"id" => id}) do user = conn.assigns[:current_user] diff --git a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/client_schema.ex b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/client_schema.ex new file mode 100644 index 00000000..723a7ad2 --- /dev/null +++ b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/client_schema.ex @@ -0,0 +1,40 @@ +defmodule ExFleetYardsAuth.Api.ClientSchema do + @moduledoc """ + Schema definitions for Oauth Clients + """ + use ExFleetYards.Schemas, :schema + + defmodule Client do + require OpenApiSpex + + OpenApiSpex.schema(%{ + description: "Oauth Client", + type: :object, + properties: %{ + access_token_ttl: %Schema{type: :integer, example: 86400}, + authorization_code_ttl: %Schema{type: :integer, example: 60}, + id: %Schema{type: :string, format: :uuid}, + id_token_ttl: %Schema{type: :integer, example: 86400}, + name: %Schema{type: :string}, + pkce: %Schema{type: :boolean}, + redirect_uris: %Schema{type: :array, items: %Schema{type: :string, format: :uri}}, + refresh_token_ttl: %Schema{type: :integer, example: 86400}, + supported_grant_types: %Schema{type: :array, items: %Schema{type: :string}}, + secret: %Schema{type: :string} + }, + required: [:id, :name] + }) + end + + defmodule ClientList do + require OpenApiSpex + + OpenApiSpex.schema(%{ + description: "List of Oauth Clients", + type: :array, + items: Client + }) + end + + result(ClientDelete, "Client Delete", %{client: ExFleetYardsAuth.Api.ClientSchema.Client}, []) +end diff --git a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/totp_controller.ex b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/totp_controller.ex index 124eac79..212b5ccf 100644 --- a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/totp_controller.ex +++ b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/totp_controller.ex @@ -2,13 +2,21 @@ defmodule ExFleetYardsAuth.Api.TotpController do @moduledoc """ Totp controller """ - use ExFleetYardsAuth, :controller + use ExFleetYardsAuth, :controller_api require Logger - import ExFleetYards.Plugs.ApiAuthorization, only: [authorize: 2] + alias ExFleetYardsAuth.Api.TotpSchema alias ExFleetYards.Repo.Account.User plug(:authorize, ["user:security"]) + security [%{"authorization" => ["user:security"]}] + tags ["user", "security"] + + operation :index, + summary: "Returns if user has totp setup", + responses: [ + ok: {"UserHasTotp", "application/json", TotpSchema.UserHasTotp} + ] def index(conn, _params) do user = conn.assigns[:current_user] @@ -19,6 +27,13 @@ defmodule ExFleetYardsAuth.Api.TotpController do |> json(%{has_totp: totp}) end + operation :delete, + summary: "Delete totp for user", + responses: [ + ok: {"Result", "application/json", Result}, + not_found: {"Result", "application/json", Result} + ] + def delete(conn, _params) do user = conn.assigns[:current_user] @@ -38,12 +53,19 @@ defmodule ExFleetYardsAuth.Api.TotpController do end end + operation :create, + summary: "Create totp secret for user", + responses: [ + ok: {"TotpSecret", "application/json", TotpSchema.TotpSecret}, + bad_request: {"Result", "application/json", Result} + ] + def create(conn, _params) do user = conn.assigns[:current_user] if ExFleetYards.Repo.Account.User.Totp.exists?(user.id) do conn - |> put_status(400) + |> put_status(:bad_request) |> json(%{"code" => "already_exists", "message" => "totp already exists"}) else totp = @@ -55,6 +77,13 @@ defmodule ExFleetYardsAuth.Api.TotpController do end end + operation :put, + summary: "Put totp secret for user", + responses: [ + created: {"TotpRecovery", "application/json", TotpSchema.TotpRecovery}, + bad_request: {"Result", "application/json", Result} + ] + def put(conn, %{"secret" => secret}) do user = conn.assigns[:current_user] @@ -71,12 +100,12 @@ defmodule ExFleetYardsAuth.Api.TotpController do else true -> conn - |> put_status(400) + |> put_status(:bad_request) |> json(%{"code" => "already_exists", "message" => "totp already exists"}) :error -> conn - |> put_status(400) + |> put_status(:bad_request) |> json(%{"code" => "invalid_secret", "message" => "invalid secret"}) end end diff --git a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/totp_schema.ex b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/totp_schema.ex new file mode 100644 index 00000000..88fcb528 --- /dev/null +++ b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/api/totp_schema.ex @@ -0,0 +1,38 @@ +defmodule ExFleetYardsAuth.Api.TotpSchema do + @moduledoc """ + Schema definitions for TOTP + """ + use ExFleetYards.Schemas, :schema + + defmodule UserHasTotp do + require OpenApiSpex + + OpenApiSpex.schema(%{ + description: "User has totp", + type: :object, + properties: %{ + has_totp: %Schema{type: :boolean} + }, + required: [:has_totp], + example: %{has_totp: true} + }) + end + + ExFleetYards.Schemas.result( + TotpSecret, + "Totp secret", + %{ + secret: %OpenApiSpex.Schema{type: :string} + }, + [:secret] + ) + + result( + TotpRecovery, + "Totp Recovery", + %{ + recovery_codes: %OpenApiSpex.Schema{type: :array, items: %OpenApiSpex.Schema{type: :string}} + }, + [:recovery_codes] + ) +end diff --git a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/error/error_html/503.html.heex b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/error/error_html/503.html.heex new file mode 100644 index 00000000..dded97d5 --- /dev/null +++ b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/error/error_html/503.html.heex @@ -0,0 +1,22 @@ +
+ An unexpected error occurred while processing your request. +
+ +