Packages

phoenix_kit

1.7.185
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 modules.ex
Raw

lib/phoenix_kit_web/live/modules.ex

defmodule PhoenixKitWeb.Live.Modules do
@moduledoc """
Admin modules management LiveView for PhoenixKit.
Displays available system modules and their configuration status.
All module references are resolved at runtime via the ModuleRegistry,
so removing or adding modules requires no changes to this file.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Admin.Events
alias PhoenixKit.Dashboard.Tab
alias PhoenixKit.ModuleDiscovery
alias PhoenixKit.ModuleRegistry
alias PhoenixKit.Settings
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.Date, as: UtilsDate
# ============================================================================
# Mount
# ============================================================================
def mount(_params, _session, socket) do
if connected?(socket), do: Events.subscribe_to_modules()
project_title = Settings.get_project_title()
module_configs = load_all_module_configs()
scope = socket.assigns[:phoenix_kit_current_scope]
accessible = if scope, do: Scope.accessible_modules(scope), else: MapSet.new()
external_modules = load_external_modules(module_configs)
dep_warnings = ModuleRegistry.dependency_warnings()
not_installed = ModuleRegistry.not_installed_packages()
socket =
socket
|> assign(:page_title, "Modules")
|> assign(:project_title, project_title)
|> assign(:accessible_modules, accessible)
|> assign(:module_configs, module_configs)
|> assign(:external_modules, external_modules)
|> assign(:dep_warnings, dep_warnings)
|> assign(:not_installed_packages, not_installed)
{:ok, socket}
end
# ============================================================================
# Toggle Events
# ============================================================================
# All toggle events go through authorize_toggle/2 first.
def handle_event("toggle_module", %{"key" => key}, socket) do
case authorize_toggle(socket, key) do
:ok -> dispatch_toggle(socket, key)
{:error, :access_denied} -> {:noreply, put_flash(socket, :error, "Access denied")}
end
end
# ============================================================================
# PubSub Handlers
# ============================================================================
def handle_info({:module_enabled, module_key}, socket) do
socket =
socket
|> reload_module_config(module_key)
|> assign(:dep_warnings, ModuleRegistry.dependency_warnings())
{:noreply, socket}
end
def handle_info({:module_disabled, module_key}, socket) do
socket =
socket
|> reload_module_config(module_key)
|> assign(:dep_warnings, ModuleRegistry.dependency_warnings())
{:noreply, socket}
end
def handle_info(_msg, socket), do: {:noreply, socket}
# ============================================================================
# Gettext registration — module names and descriptions are loaded dynamically
# from each module's module_name/0 and permission_metadata/0 callbacks (or
# from Hex.pm metadata for not-installed packages), so the strings are not
# visible to `mix gettext.extract`. We register every known string here via
# gettext_noop/1 so translators can localize the Modules overview page.
# ============================================================================
@compile {:no_warn_undefined, :_register_module_translations}
@doc false
def _register_module_translations do
# Installed module names (module_name/0 results)
[
gettext_noop("AI"),
gettext_noop("Billing"),
gettext_noop("CRM"),
gettext_noop("Catalogue"),
gettext_noop("Comments"),
gettext_noop("Customer Support"),
gettext_noop("Document Creator"),
gettext_noop("E-Commerce"),
gettext_noop("Ecommerce"),
gettext_noop("Emails"),
gettext_noop("Entities"),
gettext_noop("Hello World"),
gettext_noop("Legal"),
gettext_noop("Locations"),
gettext_noop("Newsletters"),
gettext_noop("Posts"),
gettext_noop("Projects"),
gettext_noop("Publishing"),
gettext_noop("Staff"),
gettext_noop("Sync"),
gettext_noop("User Connections"),
# Hex humanize variants (used by Available Packages section)
gettext_noop("Ai"),
gettext_noop("Crm"),
# Module descriptions (from module_name + permission_metadata.description
# of installed externals AND Hex metadata for not-installed packages)
gettext_noop("AI endpoints, prompts, and usage tracking"),
gettext_noop(
"AI module for PhoenixKit — endpoints, prompts, completions, and usage tracking"
),
gettext_noop("Billing module for PhoenixKit — payments, subscriptions, invoices"),
gettext_noop("Blog posts, categories, and content publishing"),
gettext_noop(
"Catalogue module for PhoenixKit — manufacturers, suppliers, and product catalogues."
),
gettext_noop("Comment moderation, threading, and reactions across all content types"),
gettext_noop(
"Comments module for PhoenixKit — polymorphic threading, likes/dislikes, and moderation"
),
gettext_noop(
"CRM module for PhoenixKit — organization accounts, role-scoped user views, and per-user column configuration."
),
gettext_noop("Customer support ticketing module for PhoenixKit"),
gettext_noop("Database-backed CMS pages and multi-language content"),
gettext_noop(
"Document Creator module for PhoenixKit — document templates and PDF generation via Google Docs"
),
gettext_noop("E-commerce module for PhoenixKit — products, categories, cart, checkout"),
gettext_noop("Email tracking, analytics, and AWS SES integration for PhoenixKit"),
gettext_noop(
"Entities module for PhoenixKit — dynamic content types with flexible field schemas"
),
gettext_noop(
"Hello World demo module for PhoenixKit — use as a template for your own plugins"
),
gettext_noop(
"Legal compliance module for PhoenixKit — GDPR/CCPA legal pages, cookie consent, consent logging"
),
gettext_noop(
"Locations module for PhoenixKit — manage physical locations with custom types."
),
gettext_noop(
"Newsletters module for PhoenixKit — email broadcasts and subscription management"
),
gettext_noop("Peer-to-peer data sync module for PhoenixKit"),
gettext_noop("Peer-to-peer data synchronization and replication"),
gettext_noop(
"Posts module for PhoenixKit — blog posts, tags, groups, likes, media, and scheduling"
),
gettext_noop(
"Projects module for PhoenixKit — projects, reusable tasks, assignments, and dependencies."
),
gettext_noop(
"Publishing module for PhoenixKit — database-backed CMS with multi-language support"
),
gettext_noop("Staff module for PhoenixKit — departments, teams, and people."),
gettext_noop(
"User connections module for PhoenixKit — follows, mutual connections, and blocking"
),
# External module description fallback used when permission_metadata is missing
gettext_noop("External module")
]
end
# ============================================================================
# Helpers (used in template)
# ============================================================================
@doc "Safely get a module config value, returning default if module not loaded."
def mcfg(module_configs, key, field, default \\ nil) do
case module_configs[key] do
nil -> default
config -> Map.get(config, field, default)
end
end
def format_timestamp(nil), do: "Never"
def format_timestamp(iso_string) when is_binary(iso_string) do
case DateTime.from_iso8601(iso_string) do
{:ok, dt, _} ->
fake_user = %{user_timezone: nil}
date_str = UtilsDate.format_date_with_user_timezone(dt, fake_user)
time_str = UtilsDate.format_time_with_user_timezone(dt, fake_user)
"#{date_str} #{time_str}"
_ ->
iso_string
end
end
def format_timestamp(_), do: "Never"
def format_bytes(nil), do: "0 B"
def format_bytes(0), do: "0 B"
def format_bytes(%Decimal{} = bytes) do
bytes |> Decimal.to_integer() |> format_bytes()
end
def format_bytes(bytes) when is_integer(bytes) and bytes < 1024 do
"#{bytes} B"
end
def format_bytes(bytes) when is_integer(bytes) and bytes < 1_048_576 do
"#{Float.round(bytes / 1024, 1)} KB"
end
def format_bytes(bytes) when is_integer(bytes) and bytes < 1_073_741_824 do
"#{Float.round(bytes / 1_048_576, 1)} MB"
end
def format_bytes(bytes) when is_integer(bytes) do
"#{Float.round(bytes / 1_073_741_824, 2)} GB"
end
def format_bytes(_), do: "0 B"
# ============================================================================
# Private — Authorization
# ============================================================================
defp authorize_toggle(socket, key) do
scope = socket.assigns[:phoenix_kit_current_scope]
if scope &&
(Scope.system_role?(scope) || MapSet.member?(socket.assigns.accessible_modules, key)) do
:ok
else
{:error, :access_denied}
end
end
# Special cases with inter-module dependencies
defp dispatch_toggle(socket, "legal"), do: toggle_legal(socket)
defp dispatch_toggle(socket, "newsletters"), do: toggle_newsletters(socket)
defp dispatch_toggle(socket, "maintenance"), do: toggle_maintenance(socket)
defp dispatch_toggle(socket, key), do: generic_toggle(socket, key)
# ============================================================================
# Private — Generic Toggle
# ============================================================================
defp generic_toggle(socket, key) do
mod = ModuleRegistry.get_by_key(key)
if is_nil(mod) do
{:noreply, put_flash(socket, :error, "Module not found")}
else
configs = socket.assigns.module_configs
current_config = configs[key] || %{}
currently_enabled = current_config[:enabled] || current_config[:module_enabled] || false
new_enabled = !currently_enabled
result =
if new_enabled do
mod.enable_system()
else
mod.disable_system()
end
case normalize_result(result) do
:ok ->
if new_enabled,
do: Events.broadcast_module_enabled(key),
else: Events.broadcast_module_disabled(key)
config = mod.get_config()
configs = Map.put(socket.assigns.module_configs, key, config)
socket =
socket
|> assign(:module_configs, configs)
|> assign(:external_modules, load_external_modules(configs))
|> put_flash(
:info,
"#{mod.module_name()} #{if new_enabled, do: "enabled", else: "disabled"}"
)
{:noreply, socket}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, "Failed to update #{mod.module_name()}")}
end
end
end
# ============================================================================
# Private — Special Toggle Handlers
# ============================================================================
defp toggle_legal(socket) do
configs = socket.assigns.module_configs
legal_config = configs["legal"] || %{}
currently_enabled = legal_config[:enabled] || false
legal_mod = ModuleRegistry.get_by_key("legal")
result =
if currently_enabled,
do: legal_mod.disable_system(),
else: legal_mod.enable_system()
case result do
{:error, :publishing_required} ->
{:noreply, put_flash(socket, :error, gettext("Please enable Publishing module first"))}
other ->
case normalize_result(other) do
:ok ->
if currently_enabled,
do: Events.broadcast_module_disabled("legal"),
else: Events.broadcast_module_enabled("legal")
config = legal_mod.get_config()
label =
if currently_enabled,
do: gettext("Legal module disabled"),
else: gettext("Legal module enabled")
configs = Map.put(socket.assigns.module_configs, "legal", config)
{:noreply,
socket
|> assign(:module_configs, configs)
|> assign(:external_modules, load_external_modules(configs))
|> put_flash(:info, label)}
{:error, _} ->
action = if currently_enabled, do: "disable", else: "enable"
{:noreply,
put_flash(
socket,
:error,
gettext("Failed to %{action} Legal module", action: action)
)}
end
end
end
defp toggle_newsletters(socket) do
configs = socket.assigns.module_configs
newsletters_config = configs["newsletters"] || %{}
newsletters_enabled = newsletters_config[:enabled] || false
emails_enabled = (configs["emails"] || %{})[:enabled] || false
if newsletters_enabled do
# Disabling — always allowed
generic_toggle(socket, "newsletters")
else
# Enabling — require emails
if emails_enabled do
generic_toggle(socket, "newsletters")
else
{:noreply, put_flash(socket, :error, "Please enable Emails module first")}
end
end
end
defp toggle_maintenance(socket) do
alias PhoenixKit.Modules.Maintenance
configs = socket.assigns.module_configs
config = configs["maintenance"] || %{}
currently_enabled = config[:module_enabled] || false
if currently_enabled do
Maintenance.disable_module()
Events.broadcast_module_disabled("maintenance")
else
Maintenance.enable_module()
Events.broadcast_module_enabled("maintenance")
end
config = Maintenance.get_config()
configs = Map.put(configs, "maintenance", config)
socket =
socket
|> assign(:module_configs, configs)
|> put_flash(
:info,
"Maintenance #{if currently_enabled, do: "disabled", else: "enabled"}"
)
{:noreply, socket}
end
# ============================================================================
# Private — Config Loading
# ============================================================================
defp load_all_module_configs do
ModuleRegistry.all_modules()
|> Enum.reduce(%{}, fn mod, acc ->
with true <- Code.ensure_loaded?(mod),
true <- function_exported?(mod, :module_key, 0),
true <- function_exported?(mod, :get_config, 0) do
Map.put(acc, mod.module_key(), mod.get_config())
else
_ -> acc
end
end)
end
defp reload_module_config(socket, key) do
mod = ModuleRegistry.get_by_key(key)
if mod && Code.ensure_loaded?(mod) && function_exported?(mod, :get_config, 0) do
config = mod.get_config()
configs = Map.put(socket.assigns.module_configs, key, config)
socket
|> assign(:module_configs, configs)
|> assign(:external_modules, load_external_modules(configs))
else
socket
end
end
defp normalize_result(:ok), do: :ok
defp normalize_result({:ok, _}), do: :ok
defp normalize_result({:error, reason}), do: {:error, reason}
defp normalize_result(error), do: {:error, error}
# Build list of external/plugin modules (auto-discovered from deps).
# Each entry has the info needed to render a generic module card.
defp load_external_modules(module_configs) do
ModuleDiscovery.discover_external_modules()
|> Enum.filter(fn mod ->
Code.ensure_loaded?(mod) and function_exported?(mod, :module_key, 0)
end)
|> Enum.map(&build_external_module_data(&1, module_configs))
|> Enum.sort_by(& &1.name)
end
defp build_external_module_data(mod, module_configs) do
key = mod.module_key()
config = module_configs[key] || %{}
perm = if function_exported?(mod, :permission_metadata, 0), do: mod.permission_metadata()
%{
module: mod,
key: key,
name: mod.module_name(),
icon: (perm && perm[:icon]) || "hero-puzzle-piece",
description: (perm && perm[:description]) || "External module",
enabled: config[:enabled] || false,
config: safe_get_config(mod),
version: if(function_exported?(mod, :version, 0), do: mod.version(), else: "0.0.0"),
required_modules:
if(function_exported?(mod, :required_modules, 0), do: mod.required_modules(), else: []),
admin_links: extract_admin_links(mod),
settings_path: extract_settings_path(mod)
}
end
defp safe_get_config(mod) do
if function_exported?(mod, :get_config, 0), do: mod.get_config(), else: %{}
rescue
_ -> %{}
end
defp extract_settings_path(mod) do
if Code.ensure_loaded?(mod) and function_exported?(mod, :settings_tabs, 0) do
case mod.settings_tabs() do
[first | _] -> "/admin/settings/" <> first.path
_ -> nil
end
else
nil
end
rescue
_ -> nil
end
defp extract_admin_links(mod) do
if Code.ensure_loaded?(mod) and function_exported?(mod, :admin_tabs, 0) do
mod.admin_tabs()
|> Enum.filter(fn tab ->
tab.live_view != nil and tab.visible != false and tab.parent != nil
end)
|> Enum.uniq_by(fn tab -> tab.path end)
|> Enum.take(3)
|> Enum.map(fn tab ->
%{label: Tab.localized_label(tab), path: "/admin/" <> tab.path, icon: tab.icon}
end)
else
[]
end
end
end