Packages

phoenix_kit

1.3.2
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 settings setting.ex
Raw

lib/phoenix_kit/settings/setting.ex

defmodule PhoenixKit.Settings.Setting do
@moduledoc """
Setting schema for PhoenixKit system settings.
This schema defines system-wide settings that can be configured through
the admin panel. Settings are stored as key-value pairs with timestamps.
## Fields
- `key`: Setting identifier (unique, required)
- `value`: Setting value (string format, for simple settings)
- `value_json`: Setting value (JSONB format, for complex data structures)
- `module`: Module/feature identifier for organization (optional)
- `date_added`: When the setting was first created
- `date_updated`: When the setting was last modified
## Value Storage Strategy
Settings can use either `value` (string) OR `value_json` (JSONB), but not both:
- Use `value` for simple string settings (themes, toggles, simple config)
- Use `value_json` for complex data (objects, arrays, nested structures)
- When both are present, `value_json` takes precedence
## Default Settings
PhoenixKit includes three default system settings:
- **time_zone**: System timezone offset (default: "0" for UTC)
- **date_format**: Date display format (default: "Y-m-d")
- **time_format**: Time display format (default: "H:i" for 24-hour)
## Usage Examples
# Create a simple string setting
%Setting{}
|> Setting.changeset(%{key: "theme", value: "dark"})
|> Repo.insert()
# Create a complex JSON setting
%Setting{}
|> Setting.changeset(%{key: "app_config", value_json: %{"theme" => "dark", "features" => ["auth", "admin"]}})
|> Repo.insert()
# Update existing setting
setting
|> Setting.changeset(%{value: "light"})
|> Repo.update()
"""
use Ecto.Schema
import Ecto.Changeset
alias PhoenixKit.Users.Role
alias PhoenixKit.Users.Roles
@primary_key {:id, :id, autogenerate: true}
schema "phoenix_kit_settings" do
field :key, :string
field :value, :string
field :value_json, :map
field :module, :string
field :date_added, :utc_datetime_usec
field :date_updated, :utc_datetime_usec
end
@doc """
Creates a changeset for setting creation and updates.
Validates that key and value are present and key is unique.
Automatically sets date_updated to current time on updates.
"""
def changeset(setting, attrs) do
setting
|> cast(attrs, [:key, :value, :value_json, :module, :date_added, :date_updated])
|> validate_required([:key])
|> validate_length(:key, min: 1, max: 255)
|> validate_setting_value()
|> validate_value_exclusivity()
|> validate_length(:module, max: 255)
|> unique_constraint(:key, name: :phoenix_kit_settings_key_uidx)
|> maybe_set_timestamps()
end
@doc """
Creates a changeset for updating only the value field.
This is used when updating existing settings through the admin panel.
Automatically updates the date_updated timestamp.
"""
def update_changeset(setting, attrs) do
setting
|> cast(attrs, [:value, :value_json, :module])
|> validate_setting_value()
|> validate_value_exclusivity()
|> validate_length(:module, max: 255)
|> put_change(:date_updated, DateTime.utc_now())
end
# Private helper to set timestamps on new records
defp maybe_set_timestamps(changeset) do
case get_field(changeset, :id) do
nil ->
now = DateTime.utc_now()
changeset
|> put_change(:date_added, now)
|> put_change(:date_updated, now)
_id ->
put_change(changeset, :date_updated, DateTime.utc_now())
end
end
# Validates setting values with special handling for optional fields
defp validate_setting_value(changeset) do
key = get_field(changeset, :key)
value = get_field(changeset, :value)
value_json = get_field(changeset, :value_json)
# Skip validation if using JSON value
if value_json do
changeset
else
case key do
"site_url" ->
# site_url can be empty, but max 1000 characters
# Ensure empty string is preserved, not converted to nil
case value do
nil -> put_change(changeset, :value, "")
_ -> validate_length(changeset, :value, max: 1000)
end
_ ->
# For settings with JSON data being set, allow nil/empty value
if Map.get(changeset.changes, :value_json) do
changeset
else
# All other settings require non-empty values when using string storage
validate_length(changeset, :value, min: 1, max: 1000)
end
end
end
end
# Validates that a setting uses either value OR value_json, but not both
defp validate_value_exclusivity(changeset) do
value = get_field(changeset, :value)
value_json = get_field(changeset, :value_json)
cond do
# Both have meaningful values - only allow one
not is_nil(value) and value != "" and not is_nil(value_json) ->
add_error(changeset, :value_json, "cannot set both value and value_json, choose one")
# At least one meaningful value exists - valid
(not is_nil(value) and value != "") or not is_nil(value_json) ->
changeset
# Both are nil/empty - require at least one for new records
true ->
if is_nil(changeset.data.id) do
add_error(changeset, :value, "must provide either value or value_json")
else
changeset
end
end
end
defmodule SettingsForm do
@moduledoc """
Settings form schema for PhoenixKit system settings validation.
This embedded schema provides validation for the settings form in the admin panel.
It handles validation for core system settings like timezone, date format, and time format.
## Fields
- `project_title`: Application/project title
- `site_url`: Website URL for the application (optional)
- `allow_registration`: Allow public user registration (true/false)
- `time_zone`: System timezone offset (-12 to +12)
- `date_format`: Date display format (Y-m-d, m/d/Y, etc.)
- `time_format`: Time display format (H:i for 24-hour, h:i A for 12-hour)
- `track_registration_geolocation`: Enable IP geolocation tracking during registration (true/false)
## Usage Examples
# Create a changeset for validation
%PhoenixKit.Settings.Setting.SettingsForm{}
|> PhoenixKit.Settings.Setting.SettingsForm.changeset(%{
project_title: "My App",
time_zone: "0",
date_format: "Y-m-d",
time_format: "H:i"
})
# Validate existing settings
form_data = struct(PhoenixKit.Settings.Setting.SettingsForm, %{project_title: "My App", ...})
PhoenixKit.Settings.Setting.SettingsForm.changeset(form_data, %{time_zone: "+5"})
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field :project_title, :string
field :site_url, :string
field :allow_registration, :string
field :oauth_enabled, :string
field :oauth_google_enabled, :string
field :oauth_apple_enabled, :string
field :oauth_github_enabled, :string
field :magic_link_login_enabled, :string
field :magic_link_registration_enabled, :string
field :new_user_default_role, :string
field :week_start_day, :string
field :time_zone, :string
field :date_format, :string
field :time_format, :string
field :track_registration_geolocation, :string
# OAuth Provider Credentials
field :oauth_google_client_id, :string
field :oauth_google_client_secret, :string
field :oauth_apple_client_id, :string
field :oauth_apple_team_id, :string
field :oauth_apple_key_id, :string
field :oauth_apple_private_key, :string
field :oauth_github_client_id, :string
field :oauth_github_client_secret, :string
end
@doc """
Creates a changeset for settings form validation.
Validates that all required fields are present and have valid values.
## Validations
- All fields are required
- `project_title`: 1-100 characters
- `time_zone`: Must be a valid timezone offset (-12 to +12)
- `date_format`: Must be one of the supported formats
- `time_format`: Must be one of the supported formats
## Examples
iex> PhoenixKit.Settings.Setting.SettingsForm.changeset(%PhoenixKit.Settings.Setting.SettingsForm{}, %{project_title: "My App"})
%Ecto.Changeset{valid?: false} # Missing required fields
iex> valid_attrs = %{
...> project_title: "My App",
...> time_zone: "0",
...> date_format: "Y-m-d",
...> time_format: "H:i"
...> }
iex> PhoenixKit.Settings.Setting.SettingsForm.changeset(%PhoenixKit.Settings.Setting.SettingsForm{}, valid_attrs)
%Ecto.Changeset{valid?: true}
"""
def changeset(form, attrs) do
form
|> cast(attrs, [
:project_title,
:site_url,
:allow_registration,
:oauth_enabled,
:oauth_google_enabled,
:oauth_apple_enabled,
:oauth_github_enabled,
:magic_link_login_enabled,
:magic_link_registration_enabled,
:new_user_default_role,
:week_start_day,
:time_zone,
:date_format,
:time_format,
:track_registration_geolocation,
:oauth_google_client_id,
:oauth_google_client_secret,
:oauth_apple_client_id,
:oauth_apple_team_id,
:oauth_apple_key_id,
:oauth_apple_private_key,
:oauth_github_client_id,
:oauth_github_client_secret
])
|> validate_required([
:project_title,
:new_user_default_role,
:week_start_day,
:time_zone,
:date_format,
:time_format,
:track_registration_geolocation
])
|> validate_length(:project_title, min: 1, max: 100)
|> validate_url()
|> validate_allow_registration()
|> validate_oauth_enabled()
|> validate_oauth_provider_enabled(:oauth_google_enabled)
|> validate_oauth_provider_enabled(:oauth_apple_enabled)
|> validate_oauth_provider_enabled(:oauth_github_enabled)
|> validate_magic_link_enabled(:magic_link_login_enabled)
|> validate_magic_link_enabled(:magic_link_registration_enabled)
|> validate_new_user_default_role()
|> validate_week_start_day()
|> validate_timezone()
|> validate_date_format()
|> validate_time_format()
|> validate_track_registration_geolocation()
end
# Validates URL format (optional field - allows empty)
defp validate_url(changeset) do
site_url = get_field(changeset, :site_url)
case site_url do
nil ->
changeset
"" ->
changeset
url when is_binary(url) ->
trimmed_url = String.trim(url)
if trimmed_url == "" do
changeset
else
case URI.parse(trimmed_url) do
%URI{scheme: scheme, host: host}
when scheme in ["http", "https"] and not is_nil(host) ->
put_change(changeset, :site_url, trimmed_url)
_ ->
add_error(
changeset,
:site_url,
"must be a valid URL starting with http:// or https://"
)
end
end
_ ->
add_error(changeset, :site_url, "must be a valid URL")
end
end
# Validates allow_registration is a valid boolean string
defp validate_allow_registration(changeset) do
validate_inclusion(changeset, :allow_registration, ["true", "false"],
message: "must be either 'true' or 'false'"
)
end
# Validates new_user_default_role is a valid non-Owner role
defp validate_new_user_default_role(changeset) do
owner_role = Role.system_roles().owner
# Get all valid role names except Owner
all_roles = Roles.list_roles()
valid_roles =
all_roles
|> Enum.reject(fn role -> role.name == owner_role end)
|> Enum.map(fn role -> role.name end)
validate_inclusion(changeset, :new_user_default_role, valid_roles,
message: "must be a valid role (Owner is reserved for first user)"
)
end
# Validates week_start_day is a valid weekday number (1-7)
defp validate_week_start_day(changeset) do
validate_inclusion(changeset, :week_start_day, ["1", "2", "3", "4", "5", "6", "7"],
message: "must be a valid weekday (1-7)"
)
end
# Validates timezone offset is within acceptable range
defp validate_timezone(changeset) do
validate_change(changeset, :time_zone, fn :time_zone, time_zone ->
case Integer.parse(time_zone) do
{offset, ""} when offset >= -12 and offset <= 12 ->
[]
_ ->
[time_zone: "must be a valid timezone offset between -12 and +12"]
end
end)
end
# Validates date format is one of the supported formats
defp validate_date_format(changeset) do
supported_formats = ["Y-m-d", "m/d/Y", "d/m/Y", "d.m.Y", "d-m-Y", "F j, Y"]
validate_inclusion(changeset, :date_format, supported_formats,
message: "must be one of the supported date formats"
)
end
# Validates time format is one of the supported formats
defp validate_time_format(changeset) do
supported_formats = ["H:i", "h:i A"]
validate_inclusion(changeset, :time_format, supported_formats,
message: "must be either 24-hour (H:i) or 12-hour (h:i A) format"
)
end
# Validates track_registration_geolocation is a valid boolean string
defp validate_track_registration_geolocation(changeset) do
validate_inclusion(changeset, :track_registration_geolocation, ["true", "false"],
message: "must be either 'true' or 'false'"
)
end
# Validates oauth_enabled is a valid boolean string
defp validate_oauth_enabled(changeset) do
if get_field(changeset, :oauth_enabled) do
validate_inclusion(changeset, :oauth_enabled, ["true", "false"],
message: "must be either 'true' or 'false'"
)
else
changeset
end
end
# Validates OAuth provider enabled fields
defp validate_oauth_provider_enabled(changeset, field) do
if get_field(changeset, field) do
validate_inclusion(changeset, field, ["true", "false"],
message: "must be either 'true' or 'false'"
)
else
changeset
end
end
# Validates magic link enabled fields
defp validate_magic_link_enabled(changeset, field) do
if get_field(changeset, field) do
validate_inclusion(changeset, field, ["true", "false"],
message: "must be either 'true' or 'false'"
)
else
changeset
end
end
end
end