Packages

phoenix_kit

1.7.214
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 integrations my_integrations.ex
Raw

lib/phoenix_kit_web/live/integrations/my_integrations.ex

defmodule PhoenixKitWeb.Live.Integrations.MyIntegrations do
@moduledoc """
"My Integrations" — the current user's PERSONAL integration connections.
Lists only connections owned by the logged-in user (API keys, SMTP, bot
tokens — the self-owned-secret providers). Every context call is scoped to
`owner: {:user, uuid}` where the uuid comes ONLY from the request scope,
never from params — so a user can only ever see and act on their own
connections. Gated by the independent `integrations` permission.
Sibling of the system-wide page (`Live.Settings.Integrations`), which is
gated separately by `integrations_system`. Both render with the same shared
UI (`Components.Core.IntegrationsUI`) so the two stay visually consistent.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
import PhoenixKitWeb.Components.Core.IntegrationsUI, only: [integration_status_badge: 1]
alias PhoenixKit.Integrations
alias PhoenixKit.Integrations.Events
alias PhoenixKit.Integrations.Providers
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.Routes
@impl true
def mount(_params, _session, socket) do
user_uuid = Scope.user_uuid(socket.assigns[:phoenix_kit_current_scope])
if connected?(socket) and is_binary(user_uuid) do
Events.subscribe(user_uuid)
end
{:ok,
socket
|> assign(:page_title, gettext("My Integrations"))
|> assign(:project_title, Settings.get_project_title())
|> assign(:url_path, Routes.path("/admin/settings/integrations"))
|> assign(:user_uuid, user_uuid)
|> assign(:validating, nil)
|> load_connections()}
end
@impl true
def handle_event("disconnect", %{"uuid" => uuid}, socket) do
Integrations.disconnect(uuid, socket.assigns.user_uuid, owner: owner(socket))
{:noreply, socket |> put_flash(:info, gettext("Disconnected")) |> load_connections()}
end
def handle_event("remove_connection", %{"uuid" => uuid}, socket) do
case Integrations.remove_connection(uuid, socket.assigns.user_uuid, owner: owner(socket)) 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
def handle_event("validate_connection", %{"uuid" => uuid}, socket) do
send(self(), {:do_validate, uuid})
{:noreply, assign(socket, :validating, uuid)}
end
@impl true
def handle_info({:do_validate, uuid}, socket) do
result =
Integrations.validate_connection(uuid, socket.assigns.user_uuid, owner: owner(socket))
Integrations.record_validation(uuid, result, owner: owner(socket))
{:noreply, socket |> assign(:validating, nil) |> load_connections()}
end
# Live updates over this user's personal topic.
def handle_info({event, _, _}, socket)
when event in [
:integration_setup_saved,
:integration_connected,
:integration_connection_added,
:integration_connection_removed,
:integration_validated
],
do: {:noreply, load_connections(socket)}
def handle_info({event, _}, socket)
when event in [:integration_disconnected],
do: {:noreply, load_connections(socket)}
def handle_info({:integration_connection_renamed, _, _, _}, socket),
do: {:noreply, load_connections(socket)}
def handle_info(_msg, socket), do: {:noreply, socket}
# ── Internals ────────────────────────────────────────────────────────
defp owner(socket), do: {:user, socket.assigns.user_uuid}
defp load_connections(%{assigns: %{user_uuid: user_uuid}} = socket) when is_binary(user_uuid) do
providers = Providers.for_scope(:personal)
provider_keys = Enum.map(providers, & &1.key)
providers_by_key = Map.new(providers, &{&1.key, &1})
all_connections = Integrations.load_all_connections(provider_keys, owner: {:user, user_uuid})
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)
assign(socket, :connections, connections)
end
defp load_connections(socket), do: assign(socket, :connections, [])
@impl true
def render(assigns) do
~H"""
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
socket={@socket}
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={gettext("My Integrations")}
page_subtitle={gettext("Your own service connections — only you can see these")}
current_path={@url_path}
project_title={@project_title}
current_locale={assigns[:current_locale]}
>
<div class="container mx-auto px-4 py-6">
<%!-- Connections table --%>
<div :if={@connections != []}>
<.table_default
id="my-integrations-table"
toggleable
items={@connections}
card_title={fn conn -> conn.provider.name end}
card_fields={
fn conn ->
fields = [
%{
label: gettext("Status"),
value:
if(@validating == conn.uuid,
do: gettext("Testing..."),
else: elem(integration_status_badge(conn.data["status"]), 1)
)
}
]
fields = fields ++ [%{label: gettext("Name"), value: conn.name}]
if conn.data["external_account_id"],
do:
fields ++
[%{label: gettext("Account"), value: conn.data["external_account_id"]}],
else: fields
end
}
>
<:toolbar_title>
<span class="text-sm text-base-content/60">
{length(@connections)} {gettext("connections")}
</span>
</:toolbar_title>
<:toolbar_actions>
<.pk_link navigate="/admin/settings/integrations/new" class="btn btn-primary btn-sm">
<.icon name="hero-plus" class="w-4 h-4" />
{gettext("Add Integration")}
</.pk_link>
</:toolbar_actions>
<:card_header :let={conn}>
<div class="flex items-center gap-2">
<.icon name={conn.provider.icon} class="w-5 h-5" />
<span class="font-medium">{conn.provider.name}</span>
<span class="badge badge-ghost badge-xs">{conn.name}</span>
</div>
</:card_header>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>{gettext("Service")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Name")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Account")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Status")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Actions")}</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<.table_default_row :for={conn <- @connections}>
<.table_default_cell>
<div class="flex items-center gap-2">
<.icon name={conn.provider.icon} class="w-4 h-4 text-base-content/60" />
<span>{conn.provider.name}</span>
</div>
</.table_default_cell>
<.table_default_cell>
{conn.name}
</.table_default_cell>
<.table_default_cell>
<span :if={conn.data["external_account_id"]} class="text-sm">
{conn.data["external_account_id"]}
</span>
<span :if={!conn.data["external_account_id"]} class="text-base-content/30"></span>
</.table_default_cell>
<.table_default_cell>
<%= if @validating == conn.uuid do %>
<span class="badge badge-sm badge-info gap-1">
<span class="loading loading-spinner loading-xs"></span>
{gettext("Testing...")}
</span>
<% else %>
<% {badge_class, badge_text} = integration_status_badge(conn.data["status"]) %>
<div>
<span class={"badge badge-sm #{badge_class}"}>{badge_text}</span>
<span
:if={conn.data["validation_status"] not in [nil, "", "ok"]}
class={[
"text-xs block mt-0.5",
if(conn.data["status"] == "error", do: "text-error", else: "text-warning")
]}
>
{conn.data["validation_status"]}
</span>
</div>
<% end %>
</.table_default_cell>
<.table_default_cell>
<.table_row_menu
id={"my-integration-menu-#{conn.uuid}"}
mode="auto"
label={gettext("Actions")}
>
<.table_row_menu_link
navigate={Routes.path("/admin/settings/integrations/#{conn.uuid}")}
icon="hero-pencil"
label={gettext("Configure")}
/>
<.table_row_menu_button
:if={conn.data["status"] in ["connected", "configured", "error"]}
phx-click="validate_connection"
phx-value-uuid={conn.uuid}
icon="hero-signal"
label={gettext("Test Connection")}
/>
<.table_row_menu_divider />
<.table_row_menu_button
phx-click="remove_connection"
phx-value-uuid={conn.uuid}
icon="hero-trash"
label={gettext("Remove")}
variant="error"
data-confirm={gettext("Remove this connection?")}
/>
</.table_row_menu>
</.table_default_cell>
</.table_default_row>
</.table_default_body>
<:card_actions :let={conn}>
<.table_row_menu
id={"my-integration-card-menu-#{conn.uuid}"}
mode="auto"
label={gettext("Actions")}
>
<.table_row_menu_link
navigate={Routes.path("/admin/settings/integrations/#{conn.uuid}")}
icon="hero-pencil"
label={gettext("Configure")}
/>
<.table_row_menu_button
:if={conn.data["status"] in ["connected", "configured", "error"]}
phx-click="validate_connection"
phx-value-uuid={conn.uuid}
icon="hero-signal"
label={gettext("Test Connection")}
/>
<.table_row_menu_divider />
<.table_row_menu_button
phx-click="remove_connection"
phx-value-uuid={conn.uuid}
icon="hero-trash"
label={gettext("Remove")}
variant="error"
data-confirm={gettext("Remove this connection?")}
/>
</.table_row_menu>
</:card_actions>
</.table_default>
</div>
<%!-- Empty state --%>
<div :if={@connections == []} class="text-center py-12">
<.icon name="hero-link" class="h-16 w-16 mx-auto text-base-content/40 mb-4" />
<h3 class="text-lg font-medium mb-2">{gettext("No personal integrations yet")}</h3>
<p class="text-base-content/70 mb-4">
{gettext("Connect your own API keys and services. Only you can see these.")}
</p>
<.pk_link navigate="/admin/settings/integrations/new" class="btn btn-primary btn-sm">
<.icon name="hero-plus" class="w-4 h-4" />
{gettext("Add Integration")}
</.pk_link>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>
"""
end
end