Packages

phoenix_kit

1.7.220
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 users.html.heex
Raw

lib/phoenix_kit_web/live/settings/users.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
socket={@socket}
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={gettext("User Settings")}
page_subtitle={gettext("Manage user registration, defaults, and custom fields")}
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="flex flex-col px-4 py-6">
<%!-- Settings Form --%>
<div class="space-y-6">
<%!-- User Configuration Settings Card --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl mb-6">{gettext("User Configuration")}</h2>
<.form
:let={_form}
for={@changeset}
id="user_settings_form"
phx-submit="save_settings"
phx-change="validate_settings"
class="space-y-6"
>
<%!-- Hidden fields for required settings from General page --%>
<%!-- Use @saved_settings to preserve database values, not @settings which may have incorrect form state --%>
<%!-- General Settings --%>
<input
type="hidden"
name="settings[project_title]"
value={@saved_settings["project_title"]}
/>
<input
type="hidden"
name="settings[site_url]"
value={@saved_settings["site_url"]}
/>
<input
type="hidden"
name="settings[week_start_day]"
value={@saved_settings["week_start_day"]}
/>
<input
type="hidden"
name="settings[time_zone]"
value={@saved_settings["time_zone"]}
/>
<input
type="hidden"
name="settings[date_format]"
value={@saved_settings["date_format"]}
/>
<input
type="hidden"
name="settings[time_format]"
value={@saved_settings["time_format"]}
/>
<.section_header icon="hero-user-plus" title={gettext("Registration")} />
<%!-- Registration Control Setting --%>
<div class="form-control w-full">
<.checkbox
name="settings[allow_registration]"
checked={@settings["allow_registration"] == "true"}
label={gettext("Anyone can register")}
wrapper_class="mb-3"
/>
<.checkbox
name="settings[require_email_confirmation]"
checked={@settings["require_email_confirmation"] == "true"}
label={gettext("Require email confirmation before using the app")}
wrapper_class="mb-3"
/>
<.checkbox
name="settings[track_registration_geolocation]"
checked={@settings["track_registration_geolocation"] == "true"}
label={gettext("Track user registration location")}
wrapper_class="mb-3"
/>
<.checkbox
name="settings[registration_show_username]"
checked={@settings["registration_show_username"] == "true"}
label={gettext("Show username field on registration")}
/>
<%!-- Organization Accounts --%>
<.checkbox
name="settings[enable_organization_accounts]"
checked={@settings["enable_organization_accounts"] == "true"}
label={gettext("Enable organization accounts")}
wrapper_class="mt-3"
/>
<.unsaved_hint dirty={
Enum.any?(
[
"allow_registration",
"require_email_confirmation",
"track_registration_geolocation",
"registration_show_username",
"enable_organization_accounts"
],
fn key -> @settings[key] != @saved_settings[key] end
)
} />
<%!-- Privacy Notice for Geolocation --%>
<%= if @settings["track_registration_geolocation"] == "true" do %>
<div class="alert alert-info text-sm mt-2 max-w-prose">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div>
<div class="font-semibold">
{gettext("Privacy Notice: IP Geolocation Data")}
</div>
<div class="text-xs">
{gettext(
"When enabled, registration IP addresses and approximate location data (country, region, city) will be collected for analytics."
)}
{gettext(
"Uses free APIs: IP-API.com (45 requests/minute) with ipapi.co (1000 requests/day) fallback. Defaults to OFF for privacy."
)}
</div>
</div>
</div>
<% end %>
</div>
<.section_header icon="hero-shield-check" title={gettext("Sessions")} />
<%!-- Site-wide session-persistence policy. The master switch also
hard-blocks the cookie server-side, not just the checkbox. --%>
<div class="form-control w-full">
<.checkbox
name="settings[remember_me_enabled]"
checked={@settings["remember_me_enabled"] == "true"}
label={gettext("Allow \"Keep me logged in\" (persistent sessions)")}
wrapper_class="mb-3"
/>
<%!-- Deliberately NOT `disabled` when the master switch is off:
<.checkbox> renders an un-disabled hidden "false" fallback
next to the box, so a disabled checkbox still submits
"false" and would silently rewrite the stored value. It
simply has no effect while persistence is off. --%>
<.checkbox
name="settings[remember_me_default]"
checked={@settings["remember_me_default"] == "true"}
label={gettext("Check it by default (users can still untick it)")}
/>
<div class="text-xs text-base-content/60 mt-1 max-w-prose">
{gettext(
"Persistent sessions last 60 days. Magic-link and social logins have no checkbox to tick, so they follow the default above. Turning the first option off hides the checkbox everywhere and keeps every login to the browser session."
)}
</div>
<.unsaved_hint dirty={
Enum.any?(
["remember_me_enabled", "remember_me_default"],
fn key -> @settings[key] != @saved_settings[key] end
)
} />
</div>
<.section_header
icon="hero-arrow-right-circle"
title={gettext("Post-Login Redirects")}
/>
<%!-- Where users land after signing in / registering. An explicit
return_to (e.g. a protected page that sent them to login)
always wins over these defaults. --%>
<div class="form-control w-full">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control w-full">
<label class="label" for="settings_after_login_path">
<span class="label-text text-base font-medium">
{gettext("After-login path")}
</span>
</label>
<.input
id="settings_after_login_path"
type="text"
name="settings[after_login_path]"
value={@settings["after_login_path"]}
placeholder="/"
/>
<div class="text-xs text-base-content/60 mt-1">
{gettext("Local path users land on after signing in.")}
</div>
</div>
<div class="form-control w-full">
<label class="label" for="settings_after_registration_path">
<span class="label-text text-base font-medium">
{gettext("After-registration path")}
</span>
</label>
<.input
id="settings_after_registration_path"
type="text"
name="settings[after_registration_path]"
value={@settings["after_registration_path"]}
placeholder={gettext("Same as after login")}
/>
<div class="text-xs text-base-content/60 mt-1">
{gettext(
"Where a freshly registered account lands. Empty = after-login path."
)}
</div>
</div>
</div>
<.unsaved_hint dirty={
Enum.any?(
["after_login_path", "after_registration_path"],
fn key -> @settings[key] != @saved_settings[key] end
)
} />
</div>
<.section_header icon="hero-user-circle" title={gettext("New User Defaults")} />
<%!-- New User Default Role + Status --%>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">
{gettext("New User Default Role")}
</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Role assigned to new user registrations")}
</span>
</label>
<.select
name="settings[new_user_default_role]"
value={@settings["new_user_default_role"]}
options={@setting_options["new_user_default_role"]}
class="focus-within:select-primary"
/>
<.unsaved_hint
dirty={
@settings["new_user_default_role"] != @saved_settings["new_user_default_role"]
}
saved={
get_option_label(
@saved_settings["new_user_default_role"],
@setting_options["new_user_default_role"]
)
}
/>
<%!-- Security Warning for Admin Role --%>
<%= if @settings["new_user_default_role"] == "Admin" do %>
<div class="alert alert-warning text-sm mt-2 max-w-prose">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<div class="font-semibold">
{gettext("Security Notice: Medium Risk")}
</div>
<div class="text-xs">
{gettext(
"All new users will receive administrative privileges and access to the admin panel."
)}
{gettext("Consider \"User\" for most installations to maintain security.")}
</div>
</div>
</div>
<% end %>
</div>
<%!-- New User Default Status --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text text-base font-medium">
{gettext("New User Default Status")}
</span>
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Active status for new user registrations")}
</span>
</label>
<.select
name="settings[new_user_default_status]"
value={@settings["new_user_default_status"]}
options={@setting_options["new_user_default_status"]}
class="focus-within:select-primary"
/>
<.unsaved_hint
dirty={
@settings["new_user_default_status"] !=
@saved_settings["new_user_default_status"]
}
saved={
get_option_label(
@saved_settings["new_user_default_status"],
@setting_options["new_user_default_status"]
)
}
/>
<%!-- Security Warning for Inactive Status --%>
<%= if @settings["new_user_default_status"] == "false" do %>
<div class="alert alert-warning text-sm mt-2 max-w-prose">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<div class="font-semibold">
{gettext("Security Notice: Inactive Default")}
</div>
<div class="text-xs">
{gettext(
"New users will be created as inactive and will not be able to log in until an admin activates their account."
)}
{gettext(
"The first user (Owner) will always be active regardless of this setting."
)}
</div>
</div>
</div>
<% end %>
</div>
</div>
<.section_header icon="hero-list-bullet" title={gettext("Custom User Fields")} />
<%!-- Custom Fields Section --%>
<div class="form-control w-full">
<label class="label">
<span class="label-text-alt text-sm text-base-content/60">
{gettext("Define additional profile fields for users")}
</span>
</label>
<%= if Enum.empty?(@field_definitions) do %>
<%!-- Empty State --%>
<div class="text-center py-8 border border-dashed border-base-300 rounded-lg">
<.icon
name="hero-clipboard-document-list"
class="w-10 h-10 mx-auto mb-4 text-base-content/30"
/>
<p class="text-base-content/60 mb-4">
{gettext("No custom fields defined yet")}
</p>
<button
type="button"
class="btn btn-primary btn-sm"
phx-click="show_add_field_form"
>
<.icon name="hero-plus" class="w-4 h-4" /> {gettext("Add First Field")}
</button>
</div>
<% else %>
<%!-- Table Display --%>
<.table_default
id="user-custom-fields-table"
variant="zebra"
size="sm"
toggleable
items={Enum.sort_by(@field_definitions, & &1["position"])}
card_title={fn field -> field["label"] end}
card_fields={
fn field ->
[
%{label: gettext("Key:"), value: field["key"]},
%{label: gettext("Type"), value: field["type"]},
%{
label: gettext("Required"),
value: if(field["required"], do: gettext("Yes"), else: gettext("No"))
},
%{
label: gettext("Status"),
value:
if(field["enabled"],
do: gettext("Enabled"),
else: gettext("Disabled")
)
},
%{
label: gettext("User Access"),
value:
if(Map.get(field, "user_accessible", true),
do: gettext("User Editable"),
else: gettext("Admin Only")
)
}
]
end
}
>
<:toolbar_title>
<span class="text-sm text-base-content/60">
{length(@field_definitions)} {gettext("Custom Fields")}
</span>
</:toolbar_title>
<:toolbar_actions>
<button
type="button"
class="btn btn-primary btn-sm"
phx-click="show_add_field_form"
>
<.icon name="hero-plus" class="w-4 h-4" /> {gettext("Add Custom Field")}
</button>
</:toolbar_actions>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>{gettext("Field")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Type")}</.table_default_header_cell>
<.table_default_header_cell>
{gettext("Required")}
</.table_default_header_cell>
<.table_default_header_cell>{gettext("Status")}</.table_default_header_cell>
<.table_default_header_cell class="whitespace-nowrap">
{gettext("User Access")}
</.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={
field <- Enum.sort_by(@field_definitions, & &1["position"])
}>
<.table_default_cell>
<div class="font-semibold">{field["label"]}</div>
<div class="text-xs text-base-content/60">
{gettext("Key:")}
<code class="bg-base-300 px-1 rounded">{field["key"]}</code>
</div>
</.table_default_cell>
<.table_default_cell>
<span class="badge badge-sm h-auto">{field["type"]}</span>
</.table_default_cell>
<.table_default_cell>
<%= if field["required"] do %>
<span class="badge badge-sm badge-warning">{gettext("Yes")}</span>
<% else %>
<span class="badge badge-sm badge-ghost">{gettext("No")}</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<%= if field["enabled"] do %>
<span class="badge badge-sm badge-success">{gettext("Enabled")}</span>
<% else %>
<span class="badge badge-sm badge-ghost">{gettext("Disabled")}</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<%= if Map.get(field, "user_accessible", true) do %>
<span class="badge badge-sm badge-success whitespace-nowrap">
{gettext("User Editable")}
</span>
<% else %>
<span class="badge badge-sm badge-warning whitespace-nowrap">
{gettext("Admin Only")}
</span>
<% end %>
</.table_default_cell>
<.table_default_cell>
<div class="flex gap-1">
<button
type="button"
class="btn btn-xs btn-ghost tooltip tooltip-bottom"
data-tip={
if field["enabled"],
do: gettext("Disable"),
else: gettext("Enable")
}
phx-click="toggle_field_enabled"
phx-value-key={field["key"]}
>
<%= if field["enabled"] do %>
<.icon name="hero-eye" class="w-3 h-3" />
<% else %>
<.icon name="hero-eye-slash" class="w-3 h-3" />
<% end %>
</button>
<button
type="button"
class="btn btn-xs btn-ghost tooltip tooltip-bottom"
data-tip={gettext("Edit")}
phx-click="show_edit_field_form"
phx-value-key={field["key"]}
>
<.icon name="hero-pencil" class="w-3 h-3" />
</button>
<button
type="button"
class="btn btn-xs btn-ghost text-error tooltip tooltip-bottom"
data-tip={gettext("Delete")}
phx-click="delete_field"
phx-value-key={field["key"]}
data-confirm={gettext("Are you sure you want to delete this field?")}
>
<.icon name="hero-trash" class="w-3 h-3" />
</button>
</div>
</.table_default_cell>
</.table_default_row>
</.table_default_body>
<:card_actions :let={field}>
<button
type="button"
class="btn btn-sm btn-ghost gap-1"
phx-click="toggle_field_enabled"
phx-value-key={field["key"]}
>
<%= if field["enabled"] do %>
<.icon name="hero-eye" class="w-4 h-4" /> {gettext("Disable")}
<% else %>
<.icon name="hero-eye-slash" class="w-4 h-4" /> {gettext("Enable")}
<% end %>
</button>
<button
type="button"
class="btn btn-sm btn-ghost gap-1"
phx-click="show_edit_field_form"
phx-value-key={field["key"]}
>
<.icon name="hero-pencil" class="w-4 h-4" /> {gettext("Edit")}
</button>
<button
type="button"
class="btn btn-sm btn-ghost text-error gap-1"
phx-click="delete_field"
phx-value-key={field["key"]}
data-confirm={gettext("Are you sure you want to delete this field?")}
>
<.icon name="hero-trash" class="w-4 h-4" /> {gettext("Delete")}
</button>
</:card_actions>
</.table_default>
<% end %>
</div>
<%!-- Action Buttons --%>
<div class="flex gap-3 pt-4">
<button
type="submit"
class={[
"btn btn-primary flex-1",
if(@saving, do: "loading", else: "")
]}
disabled={@saving}
>
<%= if @saving do %>
{gettext("Saving...")}
<% else %>
{gettext("Save User Settings")}
<% end %>
</button>
</div>
</.form>
<%!-- Field Form Modal (outside main form) --%>
<%= if @show_field_form do %>
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4">
{if @editing_field,
do: gettext("Edit Custom Field"),
else: gettext("Add Custom Field")}
</h3>
<.form :let={_f} for={%{}} phx-submit="save_field" class="space-y-4">
<%!-- Field Key --%>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("Field Key")} *</span>
</label>
<.input
type="text"
name="field[key]"
value={if @editing_field, do: @editing_field["key"], else: ""}
placeholder="phone_number"
required
disabled={!!@editing_field}
/>
<div class="label">
<span class="label-text-alt">
{gettext("Lowercase letters, numbers, underscores only")}
</span>
</div>
</div>
<%!-- Field Label --%>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("Label")} *</span>
</label>
<.input
type="text"
name="field[label]"
value={if @editing_field, do: @editing_field["label"], else: ""}
placeholder={gettext("Phone Number")}
required
/>
</div>
<%!-- Field Type --%>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("Type")} *</span>
</label>
<.select
name="field[type]"
value={@field_form_type}
phx-change="field_type_changed"
required
options={
Enum.map(
~w(text textarea number boolean date email url uuid select radio checkbox),
&{String.capitalize(&1), &1}
)
}
/>
</div>
<%!-- Required Checkbox --%>
<div class="form-control">
<.checkbox
name="field[required]"
checked={@editing_field && @editing_field["required"]}
label={gettext("Required field")}
/>
</div>
<%!-- User Accessible Checkbox --%>
<div class="form-control">
<.checkbox
name="field[user_accessible]"
checked={
if @editing_field,
do: Map.get(@editing_field, "user_accessible", true),
else: true
}
label={gettext("User can edit this field")}
>
<:description>
{gettext(
"When checked, users can view and edit this field on their settings page. When unchecked, only admins can edit it."
)}
</:description>
</.checkbox>
</div>
<%!-- Options for Select/Radio/Checkbox types --%>
<%= if @field_form_type in ~w(select radio checkbox) do %>
<div class="form-control">
<label class="label">
<span class="label-text">{gettext("Options")} *</span>
<span class="label-text-alt">
{length(@field_form_options)} {gettext("option(s)")}
</span>
</label>
<%!-- Current options list --%>
<%= if @field_form_options != [] do %>
<div class="space-y-2 mb-3">
<%= for {option, index} <- Enum.with_index(@field_form_options) do %>
<div class="flex items-center gap-2">
<input type="hidden" name="field[options][]" value={option} />
<span class="badge badge-lg flex-1 justify-start">{option}</span>
<button
type="button"
class="btn btn-xs btn-ghost text-error"
phx-click="remove_field_option"
phx-value-index={index}
>
<.icon name="hero-x-mark" class="w-4 h-4" />
</button>
</div>
<% end %>
</div>
<% end %>
<%!-- Add option input --%>
<div class="flex gap-2">
<.input
type="text"
id="new_option_input"
name="new_option_value"
value={@new_option_value}
placeholder={gettext("Enter option value")}
phx-keyup="update_new_option"
class="input-sm flex-1"
wrapper_class="contents"
/>
<button
type="button"
class="btn btn-sm btn-outline"
phx-click="add_field_option"
>
<.icon name="hero-plus" class="w-4 h-4" /> {gettext("Add")}
</button>
</div>
<div class="label">
<span class="label-text-alt">
{gettext("Type an option and press Enter or click Add")}
</span>
</div>
<%= if @field_form_options == [] do %>
<div class="text-sm text-warning mt-1">
<.icon name="hero-exclamation-triangle" class="w-4 h-4 inline" />
{gettext("At least one option is required for %{type} fields",
type: String.capitalize(@field_form_type)
)}
</div>
<% end %>
</div>
<% end %>
<%!-- Modal Actions --%>
<div class="modal-action">
<button type="submit" class="btn btn-primary">
{if @editing_field, do: gettext("Update Field"), else: gettext("Add Field")}
</button>
<button type="button" class="btn" phx-click="hide_field_form">
{gettext("Cancel")}
</button>
</div>
</.form>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>