Packages

phoenix_kit

1.7.97
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.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}
# ============================================================================
# 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.label, path: "/admin/" <> tab.path, icon: tab.icon} end)
else
[]
end
end
end