Packages

phoenix_kit

1.7.189
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 authorization.ex
Raw

lib/phoenix_kit_web/live/settings/authorization.ex

defmodule PhoenixKitWeb.Live.Settings.Authorization do
@moduledoc """
Authorization settings management LiveView for PhoenixKit.
Manages login page branding and authentication methods including
magic links and OAuth provider configuration.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Modules.Storage.URLSigner
alias PhoenixKit.Settings
alias PhoenixKit.Users.OAuthConfig
require Logger
def mount(_params, _session, socket) do
current_settings = Settings.list_all_settings()
defaults = Settings.get_defaults()
setting_options = Settings.get_setting_options()
merged_settings = Map.merge(defaults, current_settings)
changeset = Settings.change_settings(merged_settings)
socket =
socket
|> assign(:page_title, "Authorization Settings")
|> assign(:settings, merged_settings)
|> assign(:saved_settings, merged_settings)
|> assign(:setting_options, setting_options)
|> assign(:changeset, changeset)
|> assign(:saving, false)
|> assign(
:project_title,
merged_settings["project_title"] || PhoenixKit.Config.get(:project_title, "PhoenixKit")
)
|> assign(:show_media_selector, false)
|> assign(:media_selection_target, nil)
{:ok, socket}
end
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
def handle_event("validate_settings", %{"settings" => settings_params}, socket) do
changeset = Settings.validate_settings(settings_params)
socket =
socket
|> assign(:settings, settings_params)
|> assign(:changeset, changeset)
{:noreply, socket}
end
def handle_event("save_settings", %{"settings" => settings_params}, socket) do
socket = assign(socket, :saving, true)
case Settings.update_settings(settings_params) do
{:ok, updated_settings} ->
OAuthConfig.configure_providers()
changeset = Settings.change_settings(updated_settings)
socket =
socket
|> assign(:settings, updated_settings)
|> assign(:saved_settings, updated_settings)
|> assign(:changeset, changeset)
|> assign(:saving, false)
|> put_flash(:info, "Authorization settings updated successfully")
{:noreply, socket}
{:error, errors} ->
error_msg = format_error_message(errors)
socket =
socket
|> assign(:saving, false)
|> put_flash(:error, error_msg)
{:noreply, socket}
end
end
def handle_event("test_oauth", %{"provider" => provider}, socket) do
provider_atom = String.to_existing_atom(provider)
credentials = oauth_credentials_from_settings(provider_atom, socket.assigns.settings)
case OAuthConfig.test_connection(provider_atom, credentials) do
{:ok, message} ->
{:noreply, put_flash(socket, :info, message)}
{:error, message} ->
{:noreply, put_flash(socket, :error, message)}
end
end
def handle_event("reload_oauth_config", _params, socket) do
OAuthConfig.configure_providers()
{:noreply, put_flash(socket, :info, "OAuth configuration reloaded from database")}
end
def handle_event("open_media_selector", %{"target" => target}, socket) do
{:noreply,
socket
|> assign(:show_media_selector, true)
|> assign(:media_selection_target, String.to_existing_atom(target))}
end
def handle_event("clear_branding_image", %{"target" => target}, socket) do
key =
case target do
"logo" -> "auth_logo_file_uuid"
"background" -> "auth_background_image_file_uuid"
"background_mobile" -> "auth_background_image_mobile_file_uuid"
end
settings = Map.put(socket.assigns.settings, key, "")
{:noreply, assign(socket, :settings, settings)}
end
## Media selector callbacks
def handle_info({:media_selected, file_uuids}, socket) do
file_uuid = List.first(file_uuids) || ""
key =
case socket.assigns.media_selection_target do
:logo -> "auth_logo_file_uuid"
:background -> "auth_background_image_file_uuid"
:background_mobile -> "auth_background_image_mobile_file_uuid"
end
settings = Map.put(socket.assigns.settings, key, file_uuid)
{:noreply,
socket
|> assign(:settings, settings)
|> assign(:show_media_selector, false)
|> assign(:media_selection_target, nil)}
end
def handle_info({:media_selector_closed}, socket) do
{:noreply,
socket
|> assign(:show_media_selector, false)
|> assign(:media_selection_target, nil)}
end
# Helper functions
defp format_error_message(%Ecto.Changeset{} = changeset) do
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
|> Map.values()
|> List.flatten()
|> Enum.join(", ")
end
def signed_preview_url(file_uuid, variant) do
URLSigner.signed_url(file_uuid, variant)
end
def get_oauth_callback_url(settings, provider) do
site_url = settings["site_url"] || "https://example.com"
url_prefix = PhoenixKit.Config.get_url_prefix()
# "/" is the sentinel Config.get_url_prefix/0 returns for "no prefix"
# (see compute_url_prefix/0) — treat it the same as "" here, matching
# Routes.path/2 and UeberAuth.get_default_base_path/0.
base_prefix = if url_prefix == "/", do: "", else: url_prefix
"#{site_url}#{base_prefix}/users/auth/#{provider}/callback"
end
# Builds the credentials map OAuthConfig.test_connection/2 expects, from
# the unsaved in-memory form state (`socket.assigns.settings`) rather than
# the database — the "Test Credentials" button must validate what the
# admin just typed, not the last-persisted values.
defp oauth_credentials_from_settings(:google, settings) do
%{
client_id: settings["oauth_google_client_id"] || "",
client_secret: settings["oauth_google_client_secret"] || ""
}
end
defp oauth_credentials_from_settings(:github, settings) do
%{
client_id: settings["oauth_github_client_id"] || "",
client_secret: settings["oauth_github_client_secret"] || ""
}
end
defp oauth_credentials_from_settings(:facebook, settings) do
%{
app_id: settings["oauth_facebook_app_id"] || "",
app_secret: settings["oauth_facebook_app_secret"] || ""
}
end
@doc """
Collapsible per-provider OAuth setup guide: the callback-URL box with a copy
button, the provider-specific console steps (slot), and the reverse-proxy
notice. One component instead of four hand-copied ~95-line blocks.
"""
attr :callback_url, :string, required: true
attr :copy_hint, :string, required: true, doc: "e.g. \"Copy this URL to Google Cloud Console\""
slot :steps, required: true, doc: "provider-specific <li> instruction items"
def oauth_setup_instructions(assigns) do
~H"""
<div class="collapse collapse-arrow bg-base-200 mt-4">
<input type="checkbox" class="peer" />
<div class="collapse-title text-sm font-medium">
{gettext("Setup Instructions")}
</div>
<div class="collapse-content text-sm space-y-4">
<%!-- Callback URL --%>
<div class="bg-base-100 border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<.icon
name="hero-information-circle"
class="stroke-current shrink-0 h-5 w-5 text-info mt-0.5"
/>
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold text-base-content mb-2">
{gettext("Callback URL")}
</div>
<div class="flex items-center gap-2">
<code class="flex-1 px-3 py-2 bg-base-200 text-base-content border border-base-300 rounded font-mono text-xs break-all">
{@callback_url}
</code>
<button
type="button"
class="btn btn-sm btn-square btn-outline"
onclick={"navigator.clipboard.writeText('#{@callback_url}')"}
title={gettext("Copy to clipboard")}
>
<.icon name="hero-clipboard" class="h-4 w-4" />
</button>
</div>
<div class="text-xs text-base-content/60 mt-2">{@copy_hint}</div>
</div>
</div>
</div>
<%!-- Provider-specific steps --%>
<div class="space-y-3">
<ol class="list-decimal list-inside space-y-2 text-base-content/80">
{render_slot(@steps)}
</ol>
</div>
<%!-- Reverse Proxy Notice --%>
<div class="bg-base-200 border border-base-300 rounded-lg p-3 text-xs">
<div class="flex items-start gap-2">
<.icon name="hero-information-circle" class="h-4 w-4 text-info shrink-0 mt-0.5" />
<div class="text-base-content/80">
<strong class="text-base-content">
{gettext("Reverse Proxy Users:")}
</strong>
{gettext("If behind nginx/apache, ensure")}
<code class="px-1 py-0.5 bg-base-300 text-base-content rounded text-xs">
X-Forwarded-Proto
</code>
{gettext("header is set to")}
<code class="px-1 py-0.5 bg-base-300 text-base-content rounded text-xs">
https
</code>
</div>
</div>
</div>
</div>
</div>
"""
end
end