# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT

defmodule AshAuthentication.Strategy.RememberMe do
  alias __MODULE__.Dsl

  alias AshAuthentication.{
    Strategy.Custom,
    Strategy.RememberMe.Transformer,
    Strategy.RememberMe.Verifier
  }

  @moduledoc """
  Strategy for authenticating using a remember me token that has a configurable token_lifetime
  and is typically valid longer than a session token. Remember me tokens are generated by other
  strategies (e.g. Password) to allow for authentication to continue beyond the scope of the
  current session.

  In order to use remember me authentication you need to have another strategy
  enabled that supports remember me.

  ### Example Usage

  Add the Remember Me strategy to your authenticated resource and update another
  strategy to the generate the remember_me token.

  ```elixir
  defmodule MyApp.Accounts.User do
    use Ash.Resource,
      extensions: [AshAuthentication],
      domain: MyApp.Accounts

    attributes do
      uuid_primary_key :id
      attribute :email, :ci_string, allow_nil?: false
    end

    authentication do
      # Tokens are required for RememberMe
      tokens do
        enabled? true
        store_all_tokens? true
        require_token_presence_for_authentication? true
      end

      # Make sure you use the log_out_everywhere add on
      # to revoke remember me tokens on password change otherwise
      # the remember me tokens will continue to sign in users
      add_ons do
        log_out_everywhere do
          apply_on_password_change? true
        end
      end

      strategies do
        password do
          identity_field :email
          hashed_password_field :hashed_password
          hash_provider AshAuthentication.BcryptProvider
          confirmation_required? true
        end

        # Add the remember me Strategy
        remember_me :remember_me do
          sign_in_action_name :sign_in_with_remember_me. # Optional defaults to :sign_in_with_[:strategy_name]
          cookie_name :remember_me # Optional. Defaults to :remember_me
          token_lifetime {30, :days} # Optional. Defaults to {30, :days}
        end
      end
    end

    # In any of the actions used by your other strategies...
    actions do
      read :sign_in_with_password do
        ...
        # Add an argument to your form
        argument :remember_me, :boolean do
          description "Whether to generate a remember me token."
          allow_nil? true
        end

        # Add the preparation that generates the token
        prepare {AshAuthentication.Strategy.RememberMe.MaybeGenerateTokenPreparation, strategy_name: :remember_me}

        # Add the metadata map that will contain the cookie_name, token, and other values
        # after a successful sign in.
        metadata :remember_me, :map do
          description "A map that includes the token options"
          allow_nil? true
        end
      end

      read :sign_in_with_remember_me do
        description "Attempt to sign in using a remember me token."
        get? true

        argument :token, :string do
          description "The remember me token"
          allow_nil? false
          sensitive? true
        end

        # validates the provided the remember me token and generates a token for the session
        prepare AshAuthentication.Strategy.RememberMe.SignInPreparation

        metadata :token, :string do
          description "A JWT that can be used to authenticate the user."
          allow_nil? false
        end
      end
    end
  end
  ```

  When the user successfully signs in, the user.__metadata__.remember_me field will contain a map
  with the token and max_age.

  If you're using AshAuthentication.Phoenix, update your AuthController to store the cookie with
  a `put_remember_me_cookie/3` callback. Other callbacks include delete_remember_me_cookie/2,
  and delete_all_remember_me_cookies/1

  ```elixir
  defmodule MyAppWeb.AuthController do
    use MyAppWeb, :controller
    use AshAuthentication.Phoenix.Controller

    @impl AshAuthentication.Phoenix.Controller
    def put_remember_me_cookie(conn, cookie_name, %{token: token, max_age: max_age}) do
      cookie_options = %{
        max_age: max_age, # matches the token lifetime
        http_only: true, # prevents the cookie from being accessed by JavaScript
        secure: true, # only send the cookie over HTTPS
        same_site: :lax # prevents the cookie from being sent with cross-site requests
      }
      conn
      |> put_resp_cookie(cookie_name, cookie_value, cookie_options)
    end
  end
  ```

  Update your router to sign in the user with the remember me token.

  ```elixir
  defmodule MyAppWeb.Router do
    pipeline :browser do
      ...
      plug :sign_in_with_remember_me # make sure this comes before load_from_session
      plug :load_from_session
    end
  end
  ```

  You should also delete the remember me token on sign out to prevent the
  user from immediately being signed back in. This will sign them out of their
  current session, but it will not revoke remember me tokens on other devices.

  ```elixir
  defmodule MyAppWeb.AuthController do
    use MyAppWeb, :controller
    use AshAuthentication.Phoenix.Controller

    @impl AshAuthentication.Phoenix.Controller
    def sign_out(conn, _params) do
      return_to = get_session(conn, :return_to) || ~p"/"

      conn
      |> clear_session(:my_otp_app)
      |> delete_all_remember_me_cookies(:my_otp_app)
      |> put_flash(:info, "You are now signed out")
      |> redirect(to: return_to)
    end
  end
  ```

  """

  defstruct identity_field: :username,
            cookie_name: :remember_me,
            name: nil,
            registration_enabled?: false,
            resource: nil,
            token_lifetime: {30, :days},
            sign_in_action_name: nil,
            __spark_metadata__: nil

  use Custom, entity: Dsl.dsl()

  defdelegate dsl(), to: Dsl
  defdelegate transform(strategy, dsl_state), to: Transformer
  defdelegate verify(strategy, dsl_state), to: Verifier

  @type t :: %__MODULE__{
          identity_field: atom,
          name: atom,
          registration_enabled?: boolean,
          resource: module,
          identity_field: atom,
          cookie_name: atom,
          sign_in_action_name: :atom,
          token_lifetime: pos_integer,
          __spark_metadata__: Spark.Dsl.Entity.spark_meta()
        }
end
