lib/wechat/plug/hub_exposer.ex

if Code.ensure_loaded?(Plug) do
  defmodule WeChat.Plug.HubExposer do
    @moduledoc """
    用于 `Hub`(中控服务器) 提供查询 `AccessToken` 的 `HTTP` 接口

    `Hub Client` 会定期从 `Hub`(中控服务器) 的接口中获取 `AccessToken`

    使用 `Phoenix` 时,将下面的代码加到 `router` 里面:

        get "/hub/expose/:store_id/:store_key", #{inspect(__MODULE__)}, clients: [ClientsA, ...]

    使用 `PlugCowboy` 时,将下面的代码加到 `router` 里面:

        get "/hub/expose/:store_id/:store_key",
          to: #{inspect(__MODULE__)},
          init_opts: [clients: [ClientsA, ...]]

    ** 注意 **, 在暴露接口的同时,请注意安全合规使用,建议在使用前增加安全防护,例如:

        import Plug.BasicAuth
        plug :basic_auth, username: "hello", password: "secret"

        get "/hub/expose/:store_id/:store_key", #{inspect(__MODULE__)}, clients: [ClientsA, ...]

    参数说明:
      - `%{clients: clients}`: 指定 clients 相当于设置白名单
      - `%{clients: {:runtime, persistent_id}}`: 在运行时获取白名单, `:persistent_term.get(persistent_id)` 返回的为 client 列表
      - `%{runtime: persistent_id, clients: clients}`: 在运行时获取白名单, `:persistent_term.get(persistent_id)` 返回的为 transform_clients 后的值
    """
    import WeChat.Plug.Helper,
      only: [json: 2, not_found: 1, get_runtime_value: 1, get_runtime_value: 2]

    alias WeChat.Work.Agent, as: WorkAgent
    @behaviour Plug

    @valid_keys [
      "access_token",
      "js_api_ticket",
      "agent_js_api_ticket",
      "wx_card_ticket",
      "component_access_token"
    ]

    @doc false
    def init(opts) do
      opts = Map.new(opts)

      maybe_runtime = fn clients ->
        case Map.get(opts, :runtime, false) do
          true ->
            if persistent_id = Map.get(opts, :persistent_id) do
              %{runtime: persistent_id, clients: clients}
            else
              raise ArgumentError,
                    "please set persistent_id when runtime: true for using #{inspect(__MODULE__)}"
            end

          false ->
            %{clients: transform_clients(clients)}

          persistent_id ->
            %{runtime: persistent_id, clients: clients}
        end
      end

      case Map.get(opts, :clients) do
        {:runtime, persistent_id} ->
          %{clients: {:runtime, persistent_id}}

        nil ->
          raise ArgumentError, "please set clients when using #{inspect(__MODULE__)}"

        client when is_atom(client) ->
          maybe_runtime.([client])

        [] ->
          raise ArgumentError, "please set clients when using #{inspect(__MODULE__)}"

        clients when is_list(clients) ->
          maybe_runtime.(clients)
      end
    end

    @doc false
    def call(%{path_params: %{"store_id" => store_id, "store_key" => store_key}} = conn, options) do
      case options do
        %{runtime: persistent_id, clients: clients} ->
          get_runtime_value({:runtime, persistent_id}, fn -> transform_clients(clients) end)

        %{clients: {:runtime, persistent_id}} ->
          with clients when clients != :error <- get_runtime_value({:runtime, persistent_id}) do
            transform_clients(clients)
          end

        %{clients: clients} ->
          clients
      end
      |> case do
        :error ->
          not_found(conn)

        clients ->
          in_scope? =
            case Map.fetch(clients, store_id) do
              {:ok, :all} -> true
              {:ok, scope_list} when is_list(scope_list) -> store_key in scope_list
              _ -> false
            end

          json =
            with true <- in_scope?,
                 true <- store_key in @valid_keys,
                 store_key <- String.to_existing_atom(store_key),
                 store_map when store_map != nil <-
                   WeChat.Storage.Cache.get_cache({:store_map, store_id}, store_key) do
              %{error: 0, msg: "success", store_map: store_map}
            else
              _ -> %{error: 404, msg: "not found"}
            end

          json(conn, json)
      end
    rescue
      ArgumentError -> json(conn, %{error: 404, msg: "not found"})
    end

    def call(conn, _), do: not_found(conn)

    def transform_clients(clients) do
      List.wrap(clients) |> Enum.reduce(%{}, &transform_client/2)
    end

    defp transform_client(client, acc) when is_atom(client) do
      transform_client({client, :all}, acc)
    end

    defp transform_client({client, :all}, acc) do
      if match?(:work, client.app_type()) do
        Enum.into(client.agents(), acc, &{&1.cache_id, :all})
      else
        Map.put(acc, client.appid(), :all)
      end
    end

    defp transform_client({client, scope_list}, acc) when is_list(scope_list) do
      if match?(:work, client.app_type()) do
        Enum.into(scope_list, acc, fn
          {agent, :all} ->
            {WorkAgent.fetch_agent_cache_id!(client, agent), :all}

          {agent, scope_list} when is_list(scope_list) ->
            {WorkAgent.fetch_agent_cache_id!(client, agent), scope_list}
        end)
      else
        Map.put(acc, client.appid(), scope_list)
      end
    end
  end
end