diff --git a/apps/supabase_auth/lib/supabase/go_true.ex b/apps/supabase_auth/lib/supabase/go_true.ex index 0af0d80..90a2345 100644 --- a/apps/supabase_auth/lib/supabase/go_true.ex +++ b/apps/supabase_auth/lib/supabase/go_true.ex @@ -4,15 +4,11 @@ defmodule Supabase.GoTrue do import Supabase.Client, only: [is_client: 1] alias Supabase.Client - alias Supabase.Fetcher - alias Supabase.GoTrue.Endpoints - alias Supabase.GoTrue.PKCE - alias Supabase.GoTrue.Schemas.SignInRequest alias Supabase.GoTrue.Schemas.SignInWithPassword - alias Supabase.GoTrue.Schemas.SignUpRequest alias Supabase.GoTrue.Schemas.SignUpWithPassword alias Supabase.GoTrue.Session alias Supabase.GoTrue.User + alias Supabase.GoTrue.UserHandler @opaque client :: pid | module @@ -21,9 +17,7 @@ defmodule Supabase.GoTrue do @impl true def get_user(client, %Session{} = session) do with {:ok, client} <- Client.retrieve_client(client), - uri = Endpoints.user(client), - headers = Fetcher.apply_client_headers(client, session.access_token), - {:ok, response} <- Fetcher.get(uri, headers) do + {:ok, response} <- UserHandler.get_user(client, session.access_token) do User.parse(response) end end @@ -32,22 +26,8 @@ defmodule Supabase.GoTrue do def sign_in_with_password(client, credentials) when is_client(client) do with {:ok, client} <- Client.retrieve_client(client), {:ok, credentials} <- SignInWithPassword.parse(credentials), - attrs = %{ - email: credentials.email, - phone: credentials.phone, - password: credentials.password - }, - {:ok, params} <- SignInRequest.create(attrs, credentials.options) do - sign_in_request(params, client) - end - end - - defp sign_in_request(%SignInRequest{} = request, %Client{} = client) do - headers = api_headers(client) - uri = Endpoints.sign_in(client, "password") - - with {:ok, response} <- Fetcher.post(uri, request, headers) do - Session.parse(response, response["user"]) + {:ok, response} <- UserHandler.sign_in_with_password(client, credentials) do + Session.parse(response) end end @@ -55,53 +35,7 @@ defmodule Supabase.GoTrue do def sign_up(client, credentials) when is_client(client) do with {:ok, client} <- Client.retrieve_client(client), {:ok, credentials} <- SignUpWithPassword.parse(credentials) do - if client.auth.flow_type == :pkce do - sign_up_with_pkce(credentials, client) - else - sign_up_without_pkce(credentials, client) - end - end - end - - defp sign_up_with_pkce(%SignUpWithPassword{} = credentials, %Client{} = client) do - code_verifier = PKCE.generate_verifier() - code_challenge = PKCE.generate_challenge(code_verifier) - code_challenge_method = "sha256" - - attrs = %{ - email: credentials.email, - phone: credentials.phone, - password: credentials.password, - code_challenge: code_challenge, - code_challenge_method: code_challenge_method - } - - with {:ok, params} <- SignUpRequest.create(attrs, credentials.options), - {:ok, response} <- sign_up_request(params, client) do - {:ok, response, code_challenge} - end - end - - defp sign_up_without_pkce(%SignUpWithPassword{} = credentials, %Client{} = client) do - attrs = %{email: credentials.email, phone: credentials.phone, password: credentials.password} - - with {:ok, params} <- SignUpRequest.create(attrs, credentials.options), - {:ok, user} <- sign_up_request(params, client) do - {:ok, user, nil} - end - end - - defp sign_up_request(%SignUpRequest{} = request, %Client{} = client) do - # add xform and redirect_to options to request - headers = api_headers(client) - uri = Endpoints.sign_up(client) - - with {:ok, response} <- Fetcher.post(uri, request, headers) do - User.parse(response) + UserHandler.sign_up(client, credentials) end end - - defp api_headers(%Client{} = client) do - Fetcher.apply_headers(client.conn.api_key, client.conn.access_token, client.global.headers) - end end diff --git a/apps/supabase_auth/lib/supabase/go_true/endpoints.ex b/apps/supabase_auth/lib/supabase/go_true/endpoints.ex deleted file mode 100644 index af9c189..0000000 --- a/apps/supabase_auth/lib/supabase/go_true/endpoints.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule Supabase.GoTrue.Endpoints do - @moduledoc false - - alias Supabase.Client - - @grant_types ~w[password] - - def sign_in(%Client{} = client, grant_type) when grant_type in @grant_types do - query = URI.encode_query(%{grant_type: grant_type}) - Client.retrieve_auth_url(client, "/token?" <> query) - end - - def sign_up(%Client{} = client) do - Client.retrieve_auth_url(client, "/signup") - end - - def user(%Client{} = client) do - Client.retrieve_auth_url(client, "/user") - end - - def sign_out(%Client{} = client, scope) do - query = URI.encode_query(%{scope: scope}) - Client.retrieve_auth_url(client, "/logout?" <> query) - end - - def invite(%Client{} = client) do - Client.retrieve_auth_url(client, "/invite") - end - - def generate_link(%Client{} = client) do - Client.retrieve_auth_url(client, "/admin/generate_link") - end - - def create_user(%Client{} = client) do - Client.retrieve_auth_url(client, "/admin/users") - end - - def delete_user(%Client{} = client, id) do - Client.retrieve_auth_url(client, "/admin/users/#{id}") - end - - def list_users(%Client{} = client) do - Client.retrieve_auth_url(client, "/admin/users") - end -end diff --git a/apps/supabase_auth/lib/supabase/go_true/endpoints.exs b/apps/supabase_auth/lib/supabase/go_true/endpoints.exs deleted file mode 100644 index e69de29..0000000 diff --git a/apps/supabase_auth/lib/supabase/go_true/schemas/sign_in_request.ex b/apps/supabase_auth/lib/supabase/go_true/schemas/sign_in_request.ex index 6532fb7..6d612c3 100644 --- a/apps/supabase_auth/lib/supabase/go_true/schemas/sign_in_request.ex +++ b/apps/supabase_auth/lib/supabase/go_true/schemas/sign_in_request.ex @@ -7,9 +7,6 @@ defmodule Supabase.GoTrue.Schemas.SignInRequest do alias Supabase.GoTrue.Schemas.SignInWithPassword - @required_fields ~w[password]a - @optional_fields ~w[email phone]a - @derive Jason.Encoder @primary_key false embedded_schema do @@ -18,24 +15,37 @@ defmodule Supabase.GoTrue.Schemas.SignInRequest do field(:password, :string) embeds_one :gotrue_meta_security, GoTrueMetaSecurity, primary_key: false do + @derive Jason.Encoder field(:captcha_token, :string) end end - def create(attrs, nil) do + def create(%SignInWithPassword{} = signin) do + attrs = SignInWithPassword.to_sign_in_params(signin) + gotrue_meta = %__MODULE__.GoTrueMetaSecurity{captcha_token: signin.options.captcha_token} + %__MODULE__{} - |> cast(attrs, @required_fields ++ @optional_fields) - |> validate_required(@required_fields) + |> cast(attrs, [:email, :phone, :password]) + |> put_embed(:gotrue_meta_security, gotrue_meta, required: true) + |> validate_required([:password]) + |> validate_required_inclusion([:email, :phone]) |> apply_action(:insert) end - def create(attrs, %SignInWithPassword.Options{} = options) do - gotrue_meta = %__MODULE__.GoTrueMetaSecurity{captcha_token: options.captcha_token} + defp validate_required_inclusion(%{valid?: false} = c, _), do: c - %__MODULE__{} - |> cast(attrs, @required_fields ++ @optional_fields) - |> put_embed(:gotrue_meta_security, gotrue_meta, required: true) - |> validate_required(@required_fields) - |> apply_action(:insert) + defp validate_required_inclusion(changeset, fields) do + if Enum.any?(fields, &present?(changeset, &1)) do + changeset + else + changeset + |> add_error(:email, "at least an email or phone is required") + |> add_error(:phone, "at least an email or phone is required") + end + end + + defp present?(changeset, field) do + value = get_change(changeset, field) + value && value != "" end end diff --git a/apps/supabase_auth/lib/supabase/go_true/schemas/sign_in_with_password.ex b/apps/supabase_auth/lib/supabase/go_true/schemas/sign_in_with_password.ex index 9f4a9dc..cae15c8 100644 --- a/apps/supabase_auth/lib/supabase/go_true/schemas/sign_in_with_password.ex +++ b/apps/supabase_auth/lib/supabase/go_true/schemas/sign_in_with_password.ex @@ -17,14 +17,29 @@ defmodule Supabase.GoTrue.Schemas.SignInWithPassword do end end + def to_sign_in_params(%__MODULE__{} = signin) do + Map.take(signin, [:email, :phone, :password]) + end + def parse(attrs) do %__MODULE__{} |> cast(attrs, ~w[email phone password]a) |> cast_embed(:options, with: &options_changeset/2, required: false) |> validate_required([:password]) + |> maybe_put_default_options() |> apply_action(:parse) end + defp maybe_put_default_options(%{valid?: false} = c), do: c + + defp maybe_put_default_options(changeset) do + if get_embed(changeset, :options) do + changeset + else + put_embed(changeset, :options, %__MODULE__.Options{}) + end + end + defp options_changeset(options, attrs) do cast(options, attrs, ~w[email_redirect_to data captcha_token]a) end diff --git a/apps/supabase_auth/lib/supabase/go_true/schemas/sign_up_request.ex b/apps/supabase_auth/lib/supabase/go_true/schemas/sign_up_request.ex index 7fa17c5..035a24d 100644 --- a/apps/supabase_auth/lib/supabase/go_true/schemas/sign_up_request.ex +++ b/apps/supabase_auth/lib/supabase/go_true/schemas/sign_up_request.ex @@ -21,24 +21,30 @@ defmodule Supabase.GoTrue.Schemas.SignUpRequest do field(:code_challenge_method, :string) embeds_one :gotrue_meta_security, GoTrueMetaSecurity, primary_key: false do + @derive Jason.Encoder field(:captcha_token, :string) end end - def create(attrs, nil) do - %__MODULE__{} + def changeset(signup \\ %__MODULE__{}, attrs, go_true_meta) do + signup |> cast(attrs, @required_fields ++ @optional_fields) + |> put_embed(:gotrue_meta_security, go_true_meta) |> validate_required(@required_fields) |> apply_action(:insert) end - def create(attrs, %SignUpWithPassword.Options{} = options) do - go_true_meta = %__MODULE__.GoTrueMetaSecurity{captcha_token: options.captcha_token} + def create(%SignUpWithPassword{} = signup) do + attrs = SignUpWithPassword.to_sign_up_params(signup) + go_true_meta = %__MODULE__.GoTrueMetaSecurity{captcha_token: signup.options.captcha_token} - %__MODULE__{} - |> cast(attrs, @required_fields ++ @optional_fields) - |> put_assoc(:data, go_true_meta) - |> validate_required(@required_fields) - |> apply_action(:insert) + changeset(attrs, go_true_meta) + end + + def create(%SignUpWithPassword{} = signup, code_challenge, code_method) do + attrs = SignUpWithPassword.to_sign_up_params(signup, code_challenge, code_method) + go_true_meta = %__MODULE__.GoTrueMetaSecurity{captcha_token: signup.options.captcha_token} + + changeset(attrs, go_true_meta) end end diff --git a/apps/supabase_auth/lib/supabase/go_true/schemas/sign_up_with_password.ex b/apps/supabase_auth/lib/supabase/go_true/schemas/sign_up_with_password.ex index d09113c..1482765 100644 --- a/apps/supabase_auth/lib/supabase/go_true/schemas/sign_up_with_password.ex +++ b/apps/supabase_auth/lib/supabase/go_true/schemas/sign_up_with_password.ex @@ -32,15 +32,36 @@ defmodule Supabase.GoTrue.Schemas.SignUpWithPassword do end end + def to_sign_up_params(%__MODULE__{} = signup) do + Map.take(signup, [:email, :password, :phone]) + end + + def to_sign_up_params(%__MODULE__{} = signup, code_challenge, code_method) do + signup + |> to_sign_up_params() + |> Map.merge(%{code_challange: code_challenge, code_challenge_method: code_method}) + end + @spec validate(map) :: Ecto.Changeset.t() def validate(attrs) do %__MODULE__{} |> cast(attrs, [:email, :password, :phone]) |> cast_embed(:options, with: &options_changeset/2, required: false) + |> maybe_put_default_options() |> validate_email_or_phone() |> validate_required([:password]) end + defp maybe_put_default_options(%{valid?: false} = c), do: c + + defp maybe_put_default_options(changeset) do + if get_embed(changeset, :options) do + changeset + else + put_embed(changeset, :options, %__MODULE__.Options{}) + end + end + defp options_changeset(options, attrs) do cast(options, attrs, ~w[email_redirect_to data captcha_token]a) end diff --git a/apps/supabase_auth/lib/supabase/go_true/session.ex b/apps/supabase_auth/lib/supabase/go_true/session.ex index d98b833..4f19351 100644 --- a/apps/supabase_auth/lib/supabase/go_true/session.ex +++ b/apps/supabase_auth/lib/supabase/go_true/session.ex @@ -40,27 +40,7 @@ defmodule Supabase.GoTrue.Session do %__MODULE__{} |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) + |> cast_embed(:user, required: false) |> apply_action(:parse) end - - @spec parse(map, User.t() | map) :: {:ok, t} | {:error, Ecto.Changeset.t()} - def parse(attrs, user) do - with {:ok, session} <- parse(attrs) do - session - |> change() - |> maybe_put_user_assoc(user) - |> apply_action(:parse) - end - end - - defp maybe_put_user_assoc(changeset, %User{} = user) do - put_embed(changeset, :user, user, required: true) - end - - defp maybe_put_user_assoc(changeset, %{} = user) do - case User.parse(user) do - {:ok, user} -> maybe_put_user_assoc(changeset, user) - {:error, changeset} -> changeset - end - end end diff --git a/apps/supabase_auth/lib/supabase/go_true/user_handler.ex b/apps/supabase_auth/lib/supabase/go_true/user_handler.ex new file mode 100644 index 0000000..654950f --- /dev/null +++ b/apps/supabase_auth/lib/supabase/go_true/user_handler.ex @@ -0,0 +1,68 @@ +defmodule Supabase.GoTrue.UserHandler do + @moduledoc false + + alias Supabase.Client + alias Supabase.Fetcher + alias Supabase.GoTrue.PKCE + alias Supabase.GoTrue.Schemas.SignInRequest + alias Supabase.GoTrue.Schemas.SignInWithPassword + alias Supabase.GoTrue.Schemas.SignUpRequest + alias Supabase.GoTrue.Schemas.SignUpWithPassword + alias Supabase.GoTrue.User + + @single_user_uri "/user" + @sign_in_uri "/token" + @sign_up_uri "/signup" + + def get_user(%Client{} = client, access_token) do + headers = Fetcher.apply_client_headers(client, access_token) + + client + |> Client.retrieve_auth_url(@single_user_uri) + |> Fetcher.get(nil, headers, resolve_json: true) + end + + @grant_types ~w[password] + + def sign_in_with_password(%Client{} = client, %SignInWithPassword{} = signin) do + with {:ok, request} <- SignInRequest.create(signin) do + sign_in_request(client, request, "password") + end + end + + defp sign_in_request(%Client{} = client, %SignInRequest{} = request, grant_type) + when grant_type in @grant_types do + query = URI.encode_query(%{grant_type: grant_type}) + headers = Fetcher.apply_client_headers(client) + + client + |> Client.retrieve_auth_url(@sign_in_uri) + |> URI.append_query(query) + |> Fetcher.post(request, headers) + end + + @code_challenge_method "sha256" + + def sign_up(%Client{} = client, %SignUpWithPassword{} = signup) + when client.auth.flow_type == "pkce" do + code_verifier = PKCE.generate_verifier() + code_challenge = PKCE.generate_challenge(code_verifier) + + with {:ok, request} <- SignUpRequest.create(signup, code_challenge, @code_challenge_method), + headers = Fetcher.apply_client_headers(client), + endpoint = Client.retrieve_auth_url(client, @sign_up_uri), + {:ok, response} <- Fetcher.post(endpoint, request, headers), + {:ok, user} <- User.parse(response) do + {:ok, user, code_challenge} + end + end + + def sign_up(%Client{} = client, %SignUpWithPassword{} = signup) do + with {:ok, request} <- SignUpRequest.create(signup), + headers = Fetcher.apply_client_headers(client), + endpoint = Client.retrieve_auth_url(client, @sign_up_uri), + {:ok, response} <- Fetcher.post(endpoint, request, headers) do + User.parse(response) + end + end +end diff --git a/flake.nix b/flake.nix index ccbe82e..8a43f09 100644 --- a/flake.nix +++ b/flake.nix @@ -60,7 +60,7 @@ name = "supabase-potion"; shellHook = "mkdir -p $PWD/.nix-mix"; packages = with pkgs; - [beam-pkgs.elixir_1_15 mix2nix] + [beam-pkgs.elixir_1_15 mix2nix earthly] ++ lib.optional stdenv.isDarwin [ darwin.apple_sdk.frameworks.CoreServices darwin.apple_sdk.frameworks.CoreFoundation