Packages

phoenix_kit

1.7.93
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit integrations providers.ex
Raw

lib/phoenix_kit/integrations/providers.ex

defmodule PhoenixKit.Integrations.Providers do
@moduledoc """
Registry of known integration providers.
Each provider definition describes how to connect to an external service:
what auth type it uses, what fields the admin needs to fill in, and
how to validate the connection.
Providers are defined in code, not in the database. New providers are
added here as needed. External modules can also contribute providers
via the `integration_providers/0` callback on `PhoenixKit.Module`.
"""
use Gettext, backend: PhoenixKitWeb.Gettext
require Logger
alias PhoenixKit.ModuleRegistry
@type auth_type :: :oauth2 | :api_key | :key_secret | :bot_token | :credentials
@type setup_field :: %{
key: String.t(),
label: String.t(),
type: :text | :password | :textarea | :number | :select,
required: boolean(),
placeholder: String.t(),
help: String.t() | nil,
options: [%{value: String.t(), label: String.t()}] | nil
}
@type provider :: %{
key: String.t(),
name: String.t(),
description: String.t(),
icon: String.t(),
auth_type: auth_type(),
oauth_config: map() | nil,
setup_fields: [setup_field()],
capabilities: [atom()]
}
@providers_cache_key {__MODULE__, :all}
@used_by_cache_key {__MODULE__, :used_by}
@doc """
Returns all known providers, including those contributed by external modules.
Results are cached in `persistent_term` after the first call.
Call `clear_cache/0` if modules are added or removed at runtime.
"""
@spec all() :: [provider()]
def all do
case :persistent_term.get(@providers_cache_key, :miss) do
:miss ->
providers = builtin_providers() ++ external_providers()
:persistent_term.put(@providers_cache_key, providers)
providers
cached ->
cached
end
end
@doc """
Look up a single provider by key.
Accepts both plain keys (`"google"`) and named keys (`"google:personal"`) —
the name is stripped before lookup since provider definitions are per-type.
"""
@spec get(String.t()) :: provider() | nil
def get(key) when is_binary(key) do
# Strip name if present (e.g., "google:personal" -> "google")
base_key =
case String.split(key, ":", parts: 2) do
[base, _name] -> base
[base] -> base
end
Enum.find(all(), fn p -> p.key == base_key end)
end
# ---------------------------------------------------------------------------
# Built-in provider definitions
# ---------------------------------------------------------------------------
defp builtin_providers do
[
google(),
openrouter()
]
end
defp google do
%{
key: "google",
name: gettext("Google"),
description: gettext("Google Docs, Drive, Calendar, Sheets, Gmail"),
icon: "hero-cloud",
auth_type: :oauth2,
oauth_config: %{
auth_url: "https://accounts.google.com/o/oauth2/v2/auth",
token_url: "https://oauth2.googleapis.com/token",
userinfo_url: "https://www.googleapis.com/oauth2/v2/userinfo",
default_scopes:
"openid email profile https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/documents",
auth_params: %{"access_type" => "offline", "prompt" => "consent"}
},
setup_fields: [
%{
key: "client_id",
label: gettext("Client ID"),
type: :text,
required: true,
placeholder: "xxxxx.apps.googleusercontent.com",
help: gettext("From Google Cloud Console → APIs & Services → Credentials"),
options: nil
},
%{
key: "client_secret",
label: gettext("Client Secret"),
type: :password,
required: true,
placeholder: "GOCSPX-...",
help: nil,
options: nil
}
],
capabilities: [:google_docs, :google_drive, :google_calendar, :google_sheets],
instructions: [
%{
title: gettext("Create a Google Cloud project"),
steps: [
{gettext("Go to the [Google Cloud Console](https://console.cloud.google.com)"), nil},
{gettext("Create a new project or select an existing one"), nil}
]
},
%{
title: gettext("Enable required APIs"),
steps: [
{gettext(
"Go to [APIs & Services → Library](https://console.cloud.google.com/apis/library)"
), nil},
{gettext("Search for **Google Drive API**, click it, then click **Enable**"), nil},
{gettext(
"Go back to the Library and search for **Google Docs API**, click it, then click **Enable**"
), nil}
],
note:
gettext(
"Drive API handles file listing, creation, copying, and PDF export. Docs API is used for reading document content and substituting template variables."
)
},
%{
title: gettext("Set up OAuth consent"),
steps: [
{gettext(
"Go to [Branding](https://console.cloud.google.com/auth/branding) in the sidebar — fill in the **App name** and **User support email**, then save"
), nil},
{gettext(
"Go to [Audience](https://console.cloud.google.com/auth/audience) — set user type to **External** (or Internal for Google Workspace)"
), nil},
{gettext(
"Still on Audience — while the app is in **Testing** status, add the Google account you will connect as a **Test user** (this must be the same account whose Drive will store your files)"
), nil},
{gettext(
"Go to [Data Access](https://console.cloud.google.com/auth/scopes) — click **Add or Remove Scopes** and add the Drive and Docs scopes. This step may not be required — the app requests the needed scopes at connect time regardless."
), nil}
],
note:
gettext(
"Navigate to the OAuth section using the search bar or the hamburger menu: search for \"OAuth\", or go to the sidebar: **APIs & Services → OAuth consent screen**. This opens a different section with its own sidebar."
)
},
%{
title: gettext("Create an OAuth Client"),
steps: [
{gettext(
"Go to [APIs & Services → Credentials](https://console.cloud.google.com/apis/credentials)"
), nil},
{gettext("Click **Create Credentials → OAuth client ID**"), nil},
{gettext(
"Application type: **Web application** (do not select \"Desktop app\" — it won't support redirect URIs)"
), nil},
{gettext("Under **Authorized redirect URIs**, add: `{redirect_uri}`"), nil},
{gettext("Copy the **Client ID** and **Client Secret** into the form above"), nil}
]
},
%{
title: gettext("Connect and authorize"),
steps: [
{gettext("Click **Save**, then **Connect Account**"), nil},
{gettext(
"Google will show an \"unverified app\" warning — click **Advanced → Go to (app name)** to proceed"
), nil},
{gettext("Grant access to Google Docs and Google Drive"), nil},
{gettext("You'll be redirected back here once connected"), nil}
]
}
]
}
end
defp openrouter do
%{
key: "openrouter",
name: gettext("OpenRouter"),
description: gettext("AI model access via OpenRouter (100+ models)"),
icon: "hero-sparkles",
auth_type: :api_key,
oauth_config: nil,
validation: %{
url: "https://openrouter.ai/api/v1/auth/key",
method: :get,
auth_header: "Authorization",
auth_prefix: "Bearer "
},
setup_fields: [
%{
key: "api_key",
label: gettext("API Key"),
type: :password,
required: true,
placeholder: "sk-or-v1-...",
help: gettext("From openrouter.ai/keys"),
options: nil
}
],
capabilities: [:ai_completions, :ai_embeddings],
instructions: [
%{
title: gettext("Create an OpenRouter account"),
steps: [
{gettext("Go to [openrouter.ai](https://openrouter.ai) and sign up or log in"), nil},
{gettext("Navigate to [Keys](https://openrouter.ai/keys)"), nil}
]
},
%{
title: gettext("Create an API key"),
steps: [
{gettext("Click **Create Key**"), nil},
{gettext("Give it a name (e.g., your app name)"), nil},
{gettext("Copy the key and paste it into the form above"), nil}
]
},
%{
title: gettext("Add credits (optional)"),
steps: [
{gettext("Some models are free, but most require credits"), nil},
{gettext("Go to [Credits](https://openrouter.ai/credits) to add funds"), nil}
]
}
]
}
end
# ---------------------------------------------------------------------------
# External module provider contributions
# ---------------------------------------------------------------------------
defp external_providers do
ModuleRegistry.all_modules()
|> Enum.flat_map(fn mod ->
if Code.ensure_loaded?(mod) and function_exported?(mod, :integration_providers, 0) do
try do
mod.integration_providers()
rescue
e ->
Logger.warning(
"[Integrations.Providers] #{inspect(mod)}.integration_providers/0 failed: #{Exception.message(e)}"
)
[]
end
else
[]
end
end)
end
@doc """
Clears the cached provider list and used-by map.
Call this when modules are added or removed at runtime so the next
call to `all/0` or `used_by_modules/0` recomputes from the module registry.
"""
@spec clear_cache() :: :ok
def clear_cache do
:persistent_term.erase(@providers_cache_key)
:persistent_term.erase(@used_by_cache_key)
:ok
rescue
ArgumentError -> :ok
end
@doc """
Returns a map of provider_key => [module_name] showing which modules use each integration.
"""
@spec used_by_modules() :: %{String.t() => [String.t()]}
def used_by_modules do
case :persistent_term.get(@used_by_cache_key, :miss) do
:miss ->
result = compute_used_by_modules()
:persistent_term.put(@used_by_cache_key, result)
result
cached ->
cached
end
end
defp compute_used_by_modules do
ModuleRegistry.all_modules()
|> Enum.reduce(%{}, fn mod, acc ->
if Code.ensure_loaded?(mod) and function_exported?(mod, :required_integrations, 0) do
try do
integrations = mod.required_integrations()
module_name =
if function_exported?(mod, :module_name, 0), do: mod.module_name(), else: inspect(mod)
Enum.reduce(integrations, acc, fn key, inner_acc ->
Map.update(inner_acc, key, [module_name], &[module_name | &1])
end)
rescue
e ->
Logger.warning(
"[Integrations.Providers] #{inspect(mod)}.required_integrations/0 failed: #{Exception.message(e)}"
)
acc
end
else
acc
end
end)
end
end