Packages

phoenix_kit

2.12.0
2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 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_web live settings integrations.ex
Raw

lib/phoenix_kit_web/live/settings/integrations.ex

defmodule PhoenixKitWeb.Live.Settings.Integrations do
@moduledoc """
Integrations list page — shows all configured service connections.
Each connection is displayed as a card with status, connected account info,
and quick actions (disconnect, test). An "Add Integration" button links to
the form page for creating new connections.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
# Imported per-LiveView rather than from `PhoenixKitWeb, :live_view`: a host
# app that defines its own `row_link/1` would get an ambiguous import the
# moment core wired this one in project-wide.
import PhoenixKitWeb.Components.Core.RowLink, only: [row_link: 1]
alias PhoenixKit.Integrations
alias PhoenixKit.Integrations.Encryption
alias PhoenixKit.Integrations.Events
alias PhoenixKit.Integrations.Providers
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
def mount(_params, _session, socket) do
if connected?(socket), do: Events.subscribe()
project_title = Settings.get_project_title()
socket =
socket
|> assign(:page_title, gettext("Integrations"))
|> assign(:project_title, project_title)
|> assign(:current_path, get_current_path(socket.assigns.current_locale_base))
|> load_connections()
|> assign(:validating, nil)
{:ok, socket}
end
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
# ---------------------------------------------------------------------------
# Events
# ---------------------------------------------------------------------------
def handle_event("disconnect", %{"uuid" => uuid}, socket) do
Integrations.disconnect(uuid, actor_uuid(socket), owner: :system)
{:noreply,
socket
|> put_flash(:info, gettext("Disconnected"))
|> load_connections()}
end
def handle_event("validate_connection", %{"uuid" => uuid}, socket) do
send(self(), {:do_validate, uuid})
{:noreply, assign(socket, :validating, uuid)}
end
def handle_event("remove_connection", %{"uuid" => uuid}, socket) do
case Integrations.remove_connection(uuid, actor_uuid(socket), owner: :system) do
:ok ->
{:noreply,
socket
|> put_flash(:info, gettext("Connection removed"))
|> load_connections()}
{:error, _} ->
{:noreply, put_flash(socket, :error, gettext("Failed to remove connection"))}
end
end
# ---------------------------------------------------------------------------
# Async validation
# ---------------------------------------------------------------------------
def handle_info({:do_validate, uuid}, socket) do
actor = actor_uuid(socket)
result = Integrations.validate_connection(uuid, actor, owner: :system)
Integrations.record_validation(uuid, result, owner: :system)
{:noreply,
socket
|> assign(:validating, nil)
|> load_connections()}
end
# ---------------------------------------------------------------------------
# PubSub handlers
# ---------------------------------------------------------------------------
def handle_info({:integration_setup_saved, _, _}, socket),
do: {:noreply, load_connections(socket)}
def handle_info({:integration_connected, _, _}, socket),
do: {:noreply, load_connections(socket)}
def handle_info({:integration_disconnected, _}, socket),
do: {:noreply, load_connections(socket)}
def handle_info({:integration_validated, _, _}, socket),
do: {:noreply, load_connections(socket)}
def handle_info({:integration_connection_added, _, _}, socket),
do: {:noreply, load_connections(socket)}
def handle_info({:integration_connection_removed, _, _}, socket),
do: {:noreply, load_connections(socket)}
def handle_info({:integration_connection_renamed, _, _, _}, socket),
do: {:noreply, load_connections(socket)}
# Catch-all to prevent crashes from unexpected messages
def handle_info(_msg, socket), do: {:noreply, socket}
# ---------------------------------------------------------------------------
# Private
# ---------------------------------------------------------------------------
defp load_connections(socket) do
# Recomputed on every reload (mount AND every PubSub-triggered refresh
# below), not just once at mount — this is a long-lived LiveView, and an
# admin who rotates the key / flips integration_encryption_enabled while
# the page is open should see the banner update on the next connection
# event rather than only after a fresh page load. `status/0` is a pure
# config read (see its own doc), so recomputing it here is free.
socket = assign(socket, :encryption_status, Encryption.status())
# System page: only providers usable system-wide, and only SYSTEM-owned
# connections (owner: :system) — a user's personal connection never leaks here.
providers = Providers.for_scope(:system)
provider_keys = Enum.map(providers, & &1.key)
providers_by_key = Map.new(providers, &{&1.key, &1})
# Single query for all providers instead of N+1
all_connections = Integrations.load_all_connections(provider_keys, owner: :system)
connections =
Enum.flat_map(providers, fn provider ->
Map.get(all_connections, provider.key, [])
|> Enum.map(fn %{uuid: uuid, name: name, data: data} ->
%{
provider: providers_by_key[provider.key],
uuid: uuid,
name: name,
data: data
}
end)
end)
socket
|> assign(:connections, connections)
|> assign(:provider_names, join_with_and(Enum.map(providers, & &1.name)))
end
# Joins a list of names with commas, using a translated "and" before
# the final item: ["A"] → "A", ["A", "B"] → "A and B",
# ["A", "B", "C"] → "A, B and C". `gettext("and")` is extracted to the
# .pot so each locale can supply its own conjunction.
defp join_with_and([]), do: ""
defp join_with_and([single]), do: single
defp join_with_and(list) do
{init, [last]} = Enum.split(list, -1)
Enum.join(init, ", ") <> " " <> gettext("and") <> " " <> last
end
defp actor_uuid(socket) do
case socket.assigns[:phoenix_kit_current_scope] do
%{user: %{uuid: uuid}} -> uuid
_ -> nil
end
end
defp get_current_path(locale) do
Routes.path("/admin/settings/integrations/website", locale: locale)
end
defp integration_status_badge("connected"), do: {"badge-success", gettext("Connected")}
defp integration_status_badge("configured"), do: {"badge-warning", gettext("Not tested")}
defp integration_status_badge("disconnected"), do: {"badge-ghost", gettext("Not connected")}
defp integration_status_badge("error"), do: {"badge-error", gettext("Error")}
defp integration_status_badge(_), do: {"badge-ghost", gettext("Not configured")}
# Deliberately no clause for `:dedicated` — the template guards rendering
# on `@encryption_status != :dedicated`, so the healthy case never reaches
# these.
defp encryption_status_title(:legacy_secret_key_base),
do: gettext("Credentials are protected only by a shared application secret")
defp encryption_status_title(:disabled_no_key),
do: gettext("Credentials are stored in plain text")
defp encryption_status_title(:disabled_explicit),
do: gettext("Encryption is turned off for integration credentials")
# Catch-all: the template renders this banner for ANY status other than
# `:dedicated` (see the guard note above), so a future `key_status/0`
# value this page hasn't been taught about must degrade to a generic
# warning instead of a `FunctionClauseError` crashing the settings page.
defp encryption_status_title(_other),
do: gettext("Integration credential encryption needs attention")
defp encryption_status_detail(:legacy_secret_key_base) do
gettext(
"No dedicated encryption key is configured, so credentials below fall back to a key " <>
"derived from secret_key_base — a secret shared with session signing and CSRF tokens. " <>
"Anyone who can read secret_key_base can decrypt every credential here. Run " <>
"mix phoenix_kit.integrations.rotate_key to fix this."
)
end
defp encryption_status_detail(:disabled_no_key) do
gettext(
"No encryption key could be resolved. New and existing credentials below are stored as " <>
"plain text in the database."
)
end
defp encryption_status_detail(:disabled_explicit) do
gettext(
"integration_encryption_enabled is set to false. Credentials below are stored as plain " <>
"text in the database."
)
end
# See `encryption_status_title/1`'s catch-all note.
defp encryption_status_detail(_other) do
gettext(
"The current encryption status could not be described by this admin page — it may be " <>
"newer than what this page recognizes. Check PhoenixKit.Integrations.Encryption.status/0 " <>
"directly."
)
end
end