diff --git a/lib/workos/portal.ex b/lib/workos/portal.ex new file mode 100644 index 00000000..cffc63f4 --- /dev/null +++ b/lib/workos/portal.ex @@ -0,0 +1,61 @@ +defmodule WorkOS.Portal do + @moduledoc """ + Manage Portal in WorkOS. + + @see https://workos.com/docs/reference/admin-portal + """ + + alias WorkOS.Portal.Link + + @generate_portal_link_intent [ + "audit_logs", + "domain_verification", + "dsync", + "log_streams", + "sso" + ] + + @doc """ + Generates a Portal Link + + Parameter options: + + * `:organization` - An Organization identifier. (required) + * `:intent` - The intent of the Admin Portal. (required) + * `:return_url` - The URL to which WorkOS should send users when they click on the link to return to your website. + * `:success_url` - The URL to which WorkOS will redirect users to upon successfully setting up Single Sign-On or Directory Sync. + + """ + def generate_link(client \\ WorkOS.client(), _opts) + + def generate_link(_client, %{intent: intent} = _opts) + when intent not in @generate_portal_link_intent, + do: + raise(ArgumentError, + message: + "Invalid intent, must be one of the following: " <> + Enum.join(@generate_portal_link_intent, ", ") + ) + + @spec generate_link(map()) :: + WorkOS.Client.response(String.t()) + @spec generate_link(WorkOS.Client.t(), map()) :: + WorkOS.Client.response(String.t()) + def generate_link(client, opts) + when is_map_key(opts, :organization) and is_map_key(opts, :intent) do + WorkOS.Client.post( + client, + Link, + "/portal/generate_link", + %{ + organization: opts[:organization], + intent: opts[:intent], + return_url: opts[:return_url], + success_url: opts[:success_url] + } + ) + end + + def generate_link(_client, _opts), + do: raise(ArgumentError, message: "Needs both intent and organization.") +end diff --git a/lib/workos/portal/link.ex b/lib/workos/portal/link.ex new file mode 100644 index 00000000..df7a99e4 --- /dev/null +++ b/lib/workos/portal/link.ex @@ -0,0 +1,25 @@ +defmodule WorkOS.Portal.Link do + @moduledoc """ + WorkOS Portal Link struct. + """ + + @behaviour WorkOS.Castable + + @type t() :: %__MODULE__{ + link: String.t() + } + + @enforce_keys [ + :link + ] + defstruct [ + :link + ] + + @impl true + def cast(map) do + %__MODULE__{ + link: map["link"] + } + end +end diff --git a/lib/workos/portal/portal.ex b/lib/workos/portal/portal.ex deleted file mode 100644 index 6d6f4b96..00000000 --- a/lib/workos/portal/portal.ex +++ /dev/null @@ -1,52 +0,0 @@ -defmodule WorkOS.Portal do - import WorkOS.API - - @generate_link_intents ["sso", "dsync", "audit_logs", "log_streams"] - - @moduledoc """ - The Portal module provides resource methods for working with the Admin - Portal product - - @see https://workos.com/docs/admin-portal/guide - """ - - @doc """ - Generate a link to grant access to an organization's Admin Portal - - ### Parameters - - params (map) - - intent (string) The access scope for the generated Admin Portal - link. Valid values are: ["sso", "dsync", "audit_logs", "log_streams"] - - organization (string) The ID of the organization the Admin - Portal link will be generated for. - - return_url (string) The URL that the end user will be redirected to upon - exiting the generated Admin Portal. If none is provided, the default - redirect link set in your WorkOS Dashboard will be used. - - success_url (string) he URL to which WorkOS will redirect users to upon - successfully setting up Single Sign On or Directory Sync. - - ### Example - WorkOS.Portal.generate_link(%{ - intent: "sso", - organization: "org_1234" - }) - """ - def generate_link(params, opts \\ []) - - def generate_link(%{intent: intent} = _params, _opts) - when intent not in @generate_link_intents, - do: - raise(ArgumentError, - message: - "invalid intent, must be one of the following: sso, dsync, audit_logs or log_streams" - ) - - def generate_link(params, opts) - when is_map_key(params, :organization) and is_map_key(params, :intent) do - query = process_params(params, [:intent, :organization, :return_url, :success_url]) - post("/portal/generate_link", query, opts) - end - - def generate_link(_params, _opts), - do: raise(ArgumentError, message: "need both intent and organization in params") -end diff --git a/mix.exs b/mix.exs index a57f6f38..b4506f64 100755 --- a/mix.exs +++ b/mix.exs @@ -84,7 +84,8 @@ defmodule WorkOS.MixProject do [ "Core API": [ WorkOS.SSO, - WorkOS.Organizations + WorkOS.Organizations, + WorkOS.Portal, ], "Response Structs": [ WorkOS.SSO.Connection, @@ -93,6 +94,7 @@ defmodule WorkOS.MixProject do WorkOS.SSO.ProfileAndToken, WorkOS.Organizations.Organization, WorkOS.Organizations.Organization.Domain, + WorkOS.Portal.Link, WorkOS.Empty, WorkOS.Error, WorkOS.List diff --git a/test/support/portal_client_mock.ex b/test/support/portal_client_mock.ex new file mode 100644 index 00000000..94d87d86 --- /dev/null +++ b/test/support/portal_client_mock.ex @@ -0,0 +1,31 @@ +defmodule WorkOS.Portal.ClientMock do + @moduledoc false + + use ExUnit.Case + + def generate_link(context, opts \\ []) do + Tesla.Mock.mock(fn request -> + %{api_key: api_key} = context + + assert request.method == :post + assert request.url == "#{WorkOS.base_url()}/portal/generate_link" + + assert Enum.find(request.headers, &(elem(&1, 0) == "Authorization")) == + {"Authorization", "Bearer #{api_key}"} + + body = Jason.decode!(request.body) + + for {field, value} <- + Keyword.get(opts, :assert_fields, []) do + assert body[to_string(field)] == value + end + + success_body = %{ + "link" => "https://id.workos.com/portal/launch?secret=secret" + } + + {status, body} = Keyword.get(opts, :respond_with, {200, success_body}) + %Tesla.Env{status: status, body: body} + end) + end +end diff --git a/test/workos/portal_test.exs b/test/workos/portal_test.exs new file mode 100644 index 00000000..44d03edb --- /dev/null +++ b/test/workos/portal_test.exs @@ -0,0 +1,103 @@ +defmodule WorkOS.PortalTest do + use WorkOS.TestCase + + alias WorkOS.Portal.ClientMock + + setup :setup_env + + describe "generate_link" do + test "with invalid intent, raises an error", context do + opts = [ + intent: "invalid" + ] + + context |> ClientMock.generate_link(assert_fields: opts) + + assert_raise ArgumentError, fn -> + WorkOS.Portal.generate_link(opts |> Enum.into(%{})) + end + end + + test "with a valid intent and without an organization, raises an error", context do + opts = [ + intent: "sso" + ] + + context |> ClientMock.generate_link(assert_fields: opts) + + assert_raise ArgumentError, fn -> + WorkOS.Portal.generate_link(opts |> Enum.into(%{})) + end + end + + test "with a audit_logs intent, returns portal link", context do + opts = [ + intent: "audit_logs", + organization: "org_01EHQMYV6MBK39QC5PZXHY59C3" + ] + + context |> ClientMock.generate_link(assert_fields: opts) + + assert {:ok, %WorkOS.Portal.Link{link: link}} = + WorkOS.Portal.generate_link(opts |> Enum.into(%{})) + + refute is_nil(link) + end + + test "with a domain_verification intent, returns portal link", context do + opts = [ + intent: "domain_verification", + organization: "org_01EHQMYV6MBK39QC5PZXHY59C3" + ] + + context |> ClientMock.generate_link(assert_fields: opts) + + assert {:ok, %WorkOS.Portal.Link{link: link}} = + WorkOS.Portal.generate_link(opts |> Enum.into(%{})) + + refute is_nil(link) + end + + test "with a dsync intent, returns portal link", context do + opts = [ + intent: "dsync", + organization: "org_01EHQMYV6MBK39QC5PZXHY59C3" + ] + + context |> ClientMock.generate_link(assert_fields: opts) + + assert {:ok, %WorkOS.Portal.Link{link: link}} = + WorkOS.Portal.generate_link(opts |> Enum.into(%{})) + + refute is_nil(link) + end + + test "with a log_streams intent, returns portal link", context do + opts = [ + intent: "log_streams", + organization: "org_01EHQMYV6MBK39QC5PZXHY59C3" + ] + + context |> ClientMock.generate_link(assert_fields: opts) + + assert {:ok, %WorkOS.Portal.Link{link: link}} = + WorkOS.Portal.generate_link(opts |> Enum.into(%{})) + + refute is_nil(link) + end + + test "with a sso intent, returns portal link", context do + opts = [ + intent: "sso", + organization: "org_01EHQMYV6MBK39QC5PZXHY59C3" + ] + + context |> ClientMock.generate_link(assert_fields: opts) + + assert {:ok, %WorkOS.Portal.Link{link: link}} = + WorkOS.Portal.generate_link(opts |> Enum.into(%{})) + + refute is_nil(link) + end + end +end diff --git a/workos_elixir.livemd b/workos_elixir.livemd index f1cab5f4..e4888f76 100644 --- a/workos_elixir.livemd +++ b/workos_elixir.livemd @@ -195,3 +195,24 @@ Kino.nothing() organization: Kino.Input.read(organization) }) ``` + +### Admin Portal + +#### Generate a Portal Link + +Generate a Portal Link scoped to an Organization. + +```elixir +organization = Kino.Input.text("Organization") |> Kino.render() +intent = Kino.Input.text("Intent") |> Kino.render() + +Kino.nothing() +``` + +```elixir +{:ok, %WorkOS.Portal.Link{link: link}} = + WorkOS.Portal.generate_link(client, %{ + organization: Kino.Input.read(organization), + intent: Kino.Input.read(intent), + }) +```