Packages
phoenix_kit
2.13.10
2.13.11
2.13.10
2.13.9
2.13.8
2.13.7
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.1
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.7.236
1.7.235
1.7.234
1.7.233
1.7.232
1.7.231
1.7.230
1.7.229
1.7.228
1.7.227
1.7.226
1.7.225
1.7.224
1.7.223
1.7.222
1.7.221
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
Current section
Files
lib/phoenix_kit.ex
defmodule PhoenixKit do
@moduledoc """
PhoenixKit
"""
alias PhoenixKit.Config
alias PhoenixKit.Integrations.Encryption
alias PhoenixKit.Settings
alias PhoenixKit.Users.Permissions
@doc """
Returns the current version of PhoenixKit.
Read from the loaded application spec, so it always reports the version the
host actually has rather than anything written down here — the example this
replaced still claimed `"1.3.3"` several majors later.
PhoenixKit.version()
#=> "2.5.0"
"""
@spec version() :: String.t()
def version do
Application.spec(:phoenix_kit, :vsn) |> to_string()
end
@doc """
Validates if PhoenixKit is properly configured.
Checks for required configuration keys and returns a status.
## Examples
iex> PhoenixKit.configured?()
false
"""
@spec configured?() :: boolean()
def configured? do
case Config.get(:repo, nil) do
nil -> false
_repo -> true
end
end
@doc """
Returns PhoenixKit configuration.
## Examples
iex> PhoenixKit.config()
%{ecto_repos: []}
"""
@spec config() :: map()
def config do
:phoenix_kit
|> Application.get_all_env()
|> Enum.into(%{})
end
@doc """
Final boot step — call from `Application.start/2` right after
`Supervisor.start_link/2`.
Picks up `:phoenix_kit_<x>` modules whose beams loaded after
`PhoenixKit.ModuleRegistry` initialised (a `:phoenix_kit_*` dep starts
*after* `:phoenix_kit` itself, so the registry's first scan can miss
it), then runs every registered module's `migrate_legacy/0` callback.
Returns the supervisor result unchanged so it composes:
def start(_type, _args) do
children = [...]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts) |> PhoenixKit.boot()
end
If `Supervisor.start_link/2` returned `{:error, _}`, this is a no-op —
the error passes through unchanged.
`mix phoenix_kit.install` and `mix phoenix_kit.update` wire this in
automatically; existing apps can add the call manually.
"""
@spec boot({:ok, pid()} | {:error, term()}) :: {:ok, pid()} | {:error, term()}
def boot({:ok, _pid} = result) do
harden_filter_parameters()
PhoenixKit.ModuleRegistry.rescan()
PhoenixKit.ModuleRegistry.run_all_legacy_migrations()
register_custom_permission_keys()
warn_if_integrations_encryption_insecure()
result
end
def boot({:error, _reason} = result), do: result
# Custom permission keys declared in config:
#
# config :phoenix_kit,
# custom_permission_keys: [
# {"analytics", label: "Analytics"},
# "exports"
# ]
#
# `Permissions.register_custom_key/2` has to run AFTER boot, because the Admin
# auto-grant touches the database — which is why it could not simply be read
# from config at compile time, and why every host was writing an imperative
# call at the end of its own `Application.start/2`. Admin *tabs* have been
# declarative all along; permission keys were the odd one out.
#
# ⚠️ Two things a host needs to know:
#
# * This is only read from `boot/1`, which is opt-in. A host that never calls
# it registers nothing, silently. `install`/`update` wire the call in.
# * It is read once, at boot. Changing the config needs a restart.
#
# A key that already has an admin tab carrying `permission:` is registered by
# that tab and must NOT be listed here — double-registration hits the override
# path. This is for matrix-only keys with no tab of their own.
#
# Bad entries RAISE, failing app start. Config is a deploy-time contract, and
# logging-and-skipping would hide the mistake until a colleague hit a 403 —
# exactly the failure declaring keys is meant to prevent. (`Dashboard.Registry`
# rescues instead, correctly: one bad tab should not take the dashboard down.)
defp register_custom_permission_keys do
:phoenix_kit
|> Application.get_env(:custom_permission_keys, [])
|> Enum.each(fn
{key, opts} when is_binary(key) and is_list(opts) ->
Permissions.register_custom_key(key, opts)
key when is_binary(key) ->
Permissions.register_custom_key(key)
other ->
raise ArgumentError, """
Invalid entry in config :phoenix_kit, :custom_permission_keys — #{inspect(other)}
Each entry must be a key string, or a {key, opts} tuple:
custom_permission_keys: [
"exports",
{"analytics", label: "Analytics"}
]
"""
end)
end
# One-time boot check: warns (never raises) when integration credentials
# are not protected by a dedicated encryption key. Deliberately here, not
# as a child `Task` of `PhoenixKit.Supervisor` — that supervisor commonly
# starts BEFORE the host app's own Endpoint (a generated
# `Application.start/2` lists `PhoenixKit.Supervisor` ahead of
# `MyAppWeb.Endpoint`, matching Phoenix's own convention of starting the
# Endpoint last), so `Encryption.status/0`'s Endpoint-config lookup would
# rescue a startup `ArgumentError` (the Endpoint's config ETS table
# doesn't exist yet) into `nil` and misreport the common,
# correctly-configured `secret_key_base`-fallback install as
# `:disabled_no_key` instead of `:legacy_secret_key_base`. `boot/1` runs
# only after `Supervisor.start_link/2` returns — i.e. after every child in
# the HOST's own tree has started, Endpoint included, regardless of where
# it's listed.
defp warn_if_integrations_encryption_insecure do
Encryption.warn_if_insecure()
rescue
error ->
require Logger
Logger.error(
"[PhoenixKit] Failed to check integrations encryption status at startup: #{inspect(error)}"
)
end
# `config :phoenix, :filter_parameters` is what both the endpoint's own
# request logging AND `Phoenix.LiveView.Logger` consult (via
# `Phoenix.Logger.filter_values/1`) before writing a "Parameters: ..."
# log line for every LiveView `handle_event` — including the
# Settings/Authorization form's `validate_settings`/`save_settings`,
# which carry OAuth/AWS credentials stored generically in
# `phoenix_kit_settings` (key names like `oauth_google_client_secret`).
# Found leaking those values in cleartext into a live install's log file.
#
# `filter_parameters` is a HOST-app `Application` env key: a dependency's
# own `config/config.exs` is never merged into it, so PhoenixKit cannot
# ship this as config the usual way — and asking every host to remember
# to add the line is the same silent-blacklist failure mode `settings.ex`
# already moved away from for the settings *display* side (see
# `@public_setting_keys`). Setting it once here, at boot, protects every
# host without any action on its part.
#
# `{:keep, [...]}` mode is left alone: in keep-mode anything NOT
# explicitly kept is already filtered by default, so a key we don't know
# about here is already safe.
#
# Every OTHER shape gets REPLACED, not merged into — deliberately, found
# by a destructive test (a real LiveView save, run through the real
# code path) after an earlier "merge with whatever's there" version
# silently protected nobody:
#
# `Phoenix.start/2` (`:phoenix`'s own OTP app boot — always finishes
# before the HOST's supervisor tree, and therefore before `boot/1` runs)
# unconditionally pre-compiles `:phoenix, :filter_parameters` into an
# opaque `{:compiled, key_match, value_match}` `:binary.compile_pattern/1`
# term, on EVERY boot — not only when a host configured something: Phoenix
# itself ships a package-level default env of `["password", "token"]`
# (`deps/phoenix/mix.exs`, `application/0`), so `Application.get_env/2`
# already returns non-nil before any host config is even read. That means
# by the time `boot/1` runs, this is ALWAYS already `{:compiled, ...}` —
# not a rare shape a sophisticated host opts into. There is no API to
# recover a word list from it, so "leave a compiled filter alone" is, in
# practice, "leave every host alone" — the opposite of this function's
# purpose. A plain (uncompiled) list works exactly the same at the
# `filter_values/1` call site (it just re-compiles the pattern on that
# one call instead of reusing a cached one — negligible on a settings
# save) — so overwrite with Phoenix's own documented default plus ours.
# The one real cost: a host that customized this beyond Phoenix's default
# loses that customization here.
#
# The word list is two tiers on purpose, not just the generic one:
# `password`/`token`/`secret`/`api_key` catch anything shaped like a
# credential by NAMING CONVENTION (covers a setting key nobody has
# written down as sensitive yet), while
# `PhoenixKit.Settings.restricted_setting_keys/0` — the same list
# `list_public_settings/0` uses to keep these OUT of the settings-display
# allow list — closes the gap the generic words miss:
# `aws_access_key_id` is genuinely a credential half of an AWS keypair,
# but its name contains none of `secret`/`token`/`api_key`. Duplicating
# that list by hand here instead would drift from it exactly the way the
# settings-display side used to drift from `module == "integrations"`.
defp harden_filter_parameters do
case Application.get_env(:phoenix, :filter_parameters, ["password"]) do
{:keep, _} = keep_mode ->
keep_mode
_other ->
filter =
Enum.uniq(~w(password token secret api_key) ++ Settings.restricted_setting_keys())
Application.put_env(:phoenix, :filter_parameters, filter)
end
rescue
error ->
require Logger
Logger.error(
"[PhoenixKit] Failed to harden :phoenix, :filter_parameters at startup: #{inspect(error)}"
)
end
end