Packages
phoenix_kit
1.7.61
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_web/integration.ex
# credo:disable-for-this-file Credo.Check.Refactor.LongQuoteBlocks
defmodule PhoenixKitWeb.Integration do
@moduledoc """
Integration helpers for adding PhoenixKit to Phoenix applications.
## Basic Usage
Add to your router:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
import PhoenixKitWeb.Integration
# Add PhoenixKit routes
phoenix_kit_routes() # Default: /phoenix_kit prefix
end
## Automatic Integration
When you run `mix phoenix_kit.install`, the following is automatically added to your
`:browser` pipeline:
plug PhoenixKitWeb.Plugs.Integration
This plug handles all PhoenixKit features including maintenance mode, and ensures
they work across your entire application
## Layout Integration
Configure parent layouts in config.exs:
config :phoenix_kit,
repo: MyApp.Repo,
layout: {MyAppWeb.Layouts, :app},
root_layout: {MyAppWeb.Layouts, :root}
## Authentication Callbacks
Use in your app's live_sessions:
- `:phoenix_kit_mount_current_scope` - Mounts user and scope (recommended)
- `:phoenix_kit_ensure_authenticated_scope` - Requires authentication
- `:phoenix_kit_redirect_if_authenticated_scope` - Redirects if logged in
## Routes Created
Authentication routes:
- /users/register, /users/log-in, /users/magic-link
- /users/reset-password, /users/confirm
- /users/log-out (GET/DELETE)
User dashboard routes (if enabled, default: true):
- /dashboard, /dashboard/settings
- /dashboard/settings/confirm-email/:token
Admin routes (Owner/Admin only):
- /admin, /admin/users, /admin/users/roles
- /admin/users/live_sessions, /admin/users/sessions
- /admin/settings, /admin/modules
Public pages routes (if Pages module enabled):
- {prefix}/pages/* (explicit prefix - e.g., /phoenix_kit/pages/test)
- /* (catch-all at root level - e.g., /test, /blog/post)
- Both routes serve published pages from priv/static/pages/*.md
- The catch-all can optionally serve a custom 404 markdown file when enabled
- Example: /test or /phoenix_kit/pages/test renders test.md
## Configuration
You can disable the user dashboard by setting the environment variable in your config:
# config/dev.exs or config/runtime.exs
config :phoenix_kit, user_dashboard_enabled: false
This will disable all dashboard routes (/dashboard/*). Users trying to access
the dashboard will get a 404 error.
## DaisyUI Setup
1. Install: `npm install daisyui@latest`
2. Add to tailwind.config.js:
- Content: `"../../deps/phoenix_kit"`
- Plugin: `require('daisyui')`
## Layout Templates
Use `{@inner_content}` not `render_slot(@inner_block)`:
<%!-- Correct --%>
<main>{@inner_content}</main>
## Scope Usage in Templates
<%= if PhoenixKit.Users.Auth.Scope.authenticated?(@phoenix_kit_current_scope) do %>
Welcome, {PhoenixKit.Users.Auth.Scope.user_email(@phoenix_kit_current_scope)}!
<% end %>
"""
alias PhoenixKitWeb
alias PhoenixKitWeb.Routes.BlogRoutes
alias PhoenixKitWeb.Routes.CustomerServiceRoutes
alias PhoenixKitWeb.Routes.EmailsRoutes
alias PhoenixKitWeb.Routes.PublishingRoutes
alias PhoenixKitWeb.Routes.ReferralsRoutes
alias PhoenixKitWeb.Routes.ShopRoutes
@doc """
Creates locale-aware routing scopes based on enabled languages.
This macro generates both a localized scope (e.g., `/en/`) and a non-localized
scope for backward compatibility. The locale pattern is dynamically generated
from the database-stored enabled language codes.
## Examples
locale_scope do
live "/admin", DashboardLive, :index
end
# Generates routes like:
# /phoenix_kit/en/admin (with locale)
# /phoenix_kit/admin (without locale, defaults to "en")
"""
defmacro locale_scope(opts \\ [], do: block) do
# Get URL prefix at compile time
raw_prefix =
try do
PhoenixKit.Config.get_url_prefix()
rescue
_ -> "/phoenix_kit"
end
url_prefix =
case raw_prefix do
"" -> "/"
prefix -> prefix
end
quote do
alias PhoenixKit.Modules.Languages
# Define locale validation pipeline
pipeline :phoenix_kit_locale_validation do
plug PhoenixKitWeb.Users.Auth, :phoenix_kit_validate_and_set_locale
end
# Localized scope with flexible locale pattern
# Accepts both base codes (en, es) and full dialect codes (en-US, es-MX)
# Full dialect codes are automatically redirected to base codes by the validation plug
# This ensures backward compatibility with old URLs while enforcing base code standard
scope "#{unquote(url_prefix)}/:locale",
PhoenixKitWeb,
Keyword.put(unquote(opts), :locale, ~r/^[a-z]{2}(?:-[A-Za-z0-9]{2,})?$/) do
pipe_through [:browser, :phoenix_kit_auto_setup, :phoenix_kit_locale_validation]
unquote(block)
end
# Non-localized scope for backward compatibility (defaults to "en")
scope unquote(url_prefix), PhoenixKitWeb, unquote(opts) do
pipe_through [:browser, :phoenix_kit_auto_setup, :phoenix_kit_locale_validation]
unquote(block)
end
end
end
# Helper function to generate pipeline definitions
defp generate_pipelines do
quote do
alias PhoenixKit.Modules.Languages
# Define the auto-setup pipeline
pipeline :phoenix_kit_auto_setup do
plug PhoenixKitWeb.Plugs.RequestTimer
plug PhoenixKitWeb.Users.Auth, :fetch_phoenix_kit_current_user
plug PhoenixKitWeb.Integration, :phoenix_kit_auto_setup
end
pipeline :phoenix_kit_redirect_if_authenticated do
plug PhoenixKitWeb.Users.Auth, :phoenix_kit_redirect_if_user_is_authenticated
end
pipeline :phoenix_kit_require_authenticated do
plug PhoenixKitWeb.Users.Auth, :fetch_phoenix_kit_current_user
plug PhoenixKitWeb.Users.Auth, :phoenix_kit_require_authenticated_user
end
pipeline :phoenix_kit_admin_only do
plug PhoenixKitWeb.Users.Auth, :fetch_phoenix_kit_current_user
plug PhoenixKitWeb.Users.Auth, :fetch_phoenix_kit_current_scope
plug PhoenixKitWeb.Users.Auth, :phoenix_kit_require_admin
end
# Define API pipeline for JSON endpoints
pipeline :phoenix_kit_api do
plug :accepts, ["json"]
end
# Define locale validation pipeline
pipeline :phoenix_kit_locale_validation do
plug PhoenixKitWeb.Users.Auth, :phoenix_kit_validate_and_set_locale
end
# Define shop session pipeline (ensures persistent cart session)
pipeline :phoenix_kit_shop_session do
plug PhoenixKit.Modules.Shop.Web.Plugs.ShopSession
end
end
end
# Helper function to generate basic scope routes
defp generate_basic_scope(url_prefix) do
quote do
scope unquote(url_prefix), PhoenixKitWeb do
pipe_through [:browser, :phoenix_kit_auto_setup]
post "/users/log-in", Users.Session, :create
delete "/users/log-out", Users.Session, :delete
get "/users/log-out", Users.Session, :get_logout
get "/users/magic-link/:token", Users.MagicLinkVerify, :verify
# Dashboard context switching (multi-selector with key, must come before legacy route)
post "/context/:key/:id", ContextController, :set
# Dashboard context switching (legacy single selector)
post "/context/:id", ContextController, :set
# OAuth routes for external provider authentication
get "/users/auth/:provider", Users.OAuth, :request
get "/users/auth/:provider/callback", Users.OAuth, :callback
# Magic Link Registration routes
get "/users/register/verify/:token", Users.MagicLinkRegistrationVerify, :verify
# Note: Email webhook moved to generate_emails_routes/1 (separate scope)
# Storage API routes (file upload and serving)
post "/api/upload", UploadController, :create
get "/file/:file_uuid/:variant/:token", FileController, :show
get "/api/files/:file_uuid/info", FileController, :info
# Cookie consent widget config (public API for JS auto-injection)
get "/api/consent-config", Controllers.ConsentConfigController, :config
# Pages routes temporarily disabled
# get "/pages/*path", PagesController, :show
end
# Sync API routes (JSON API - accepts JSON from remote PhoenixKit sites)
scope unquote(url_prefix) do
pipe_through [:phoenix_kit_api]
post "/sync/api/register-connection",
PhoenixKit.Modules.Sync.Web.ApiController,
:register_connection
post "/sync/api/delete-connection",
PhoenixKit.Modules.Sync.Web.ApiController,
:delete_connection
post "/sync/api/verify-connection",
PhoenixKit.Modules.Sync.Web.ApiController,
:verify_connection
post "/sync/api/update-status",
PhoenixKit.Modules.Sync.Web.ApiController,
:update_status
post "/sync/api/get-connection-status",
PhoenixKit.Modules.Sync.Web.ApiController,
:get_connection_status
post "/sync/api/list-tables",
PhoenixKit.Modules.Sync.Web.ApiController,
:list_tables
post "/sync/api/pull-data",
PhoenixKit.Modules.Sync.Web.ApiController,
:pull_data
post "/sync/api/table-schema",
PhoenixKit.Modules.Sync.Web.ApiController,
:table_schema
post "/sync/api/table-records",
PhoenixKit.Modules.Sync.Web.ApiController,
:table_records
get "/sync/api/status", PhoenixKit.Modules.Sync.Web.ApiController, :status
end
# Sync WebSocket - forward to plug for websocket upgrade handling
# Uses url_prefix to be consistent with API routes
forward "#{unquote(url_prefix)}/sync/websocket", PhoenixKit.Modules.Sync.Web.SocketPlug
# Note: Email export routes moved to generate_emails_routes/1 (separate scope)
# PhoenixKit static assets (no CSRF protection needed for static files)
scope unquote(url_prefix), PhoenixKitWeb do
pipe_through [:phoenix_kit_api]
get "/assets/:file", AssetsController, :serve
end
# Sitemap routes - public XML/XSL endpoints, no session/CSRF/auto_setup needed
scope unquote(url_prefix) do
get "/sitemap.xml", PhoenixKit.Modules.Sitemap.Web.Controller, :xml
get "/sitemap.html", PhoenixKit.Modules.Sitemap.Web.Controller, :html
get "/sitemaps/:filename", PhoenixKit.Modules.Sitemap.Web.Controller, :module_sitemap
get "/sitemap.xsl", PhoenixKit.Modules.Sitemap.Web.Controller, :xsl_stylesheet
get "/assets/sitemap/:style", PhoenixKit.Modules.Sitemap.Web.Controller, :xsl_stylesheet
get "/assets/sitemap-index/:style",
PhoenixKit.Modules.Sitemap.Web.Controller,
:xsl_index_stylesheet
end
# Billing webhook routes - uses PhoenixKit.Modules.Billing namespace (no PhoenixKitWeb prefix)
scope unquote(url_prefix) do
pipe_through [:phoenix_kit_api]
post "/webhooks/billing/stripe",
PhoenixKit.Modules.Billing.Web.WebhookController,
:stripe
post "/webhooks/billing/paypal",
PhoenixKit.Modules.Billing.Web.WebhookController,
:paypal
post "/webhooks/billing/razorpay",
PhoenixKit.Modules.Billing.Web.WebhookController,
:razorpay
end
# Shop public routes are generated via generate_shop_public_routes/1 helper
# This supports locale-prefixed URLs (/:locale/shop/...) with language switching
# Shop user dashboard routes are now in phoenix_kit_authenticated_routes/1.
end
end
# Helper function to generate catch-all root route for pages
# This allows accessing pages from the root level (e.g., /test, /blog/post)
# Must be placed at the end of the router to not interfere with other routes
defp generate_pages_catch_all do
quote do
# Catch-all route for published pages at root level
# This route should be last to avoid conflicting with app routes
# scope "/", PhoenixKitWeb do
# pipe_through [:browser, :phoenix_kit_auto_setup]
#
# # Catch-all for root-level pages (must be last route)
# get "/*path", PagesController, :show
# end
end
end
# ============================================================================
# Shared Route Definitions
# ============================================================================
# These macros generate route definitions that are shared between localized
# and non-localized scopes. This eliminates code duplication and reduces
# compile time by ~50% for router files.
# ============================================================================
# Generates unified public routes (auth + confirmation + shop) in a single live_session.
# Auth LiveViews handle the redirect-if-authenticated check in their own mount/3,
# so the shared session uses the permissive :phoenix_kit_mount_current_scope hook.
# Shop routes are included here so all public pages share one WebSocket session,
# enabling seamless LiveView navigation across auth, confirmation, and shop pages.
defmacro phoenix_kit_public_routes(suffix) do
session_name = :"phoenix_kit_public#{suffix}"
# Get shop live route declarations at compile time (no scope/pipeline wrappers)
shop_live_routes =
if suffix == :_locale do
ShopRoutes.public_live_locale_routes()
else
ShopRoutes.public_live_routes()
end
quote do
live_session unquote(session_name),
on_mount: [{PhoenixKitWeb.Users.Auth, :phoenix_kit_mount_current_scope}] do
# Auth pages — redirect-if-authenticated handled in each LiveView's mount/3
live "/users/register", Users.Registration, :new, as: :user_registration
live "/users/register/magic-link", Users.MagicLinkRegistrationRequest, :new,
as: :user_magic_link_registration_request
live "/users/register/complete/:token", Users.MagicLinkRegistration, :complete,
as: :user_magic_link_registration
live "/users/log-in", Users.Login, :new, as: :user_login
live "/users/magic-link", Users.MagicLink, :new, as: :user_magic_link
live "/users/reset-password", Users.ForgotPassword, :new, as: :user_reset_password
live "/users/reset-password/:token", Users.ResetPassword, :edit,
as: :user_reset_password_edit
# Confirmation pages — no redirect check needed
live "/users/confirm/:token", Users.Confirmation, :edit, as: :user_confirmation
live "/users/confirm", Users.ConfirmationInstructions, :new,
as: :user_confirmation_instructions
# Shop public pages — same session for seamless auth → shop navigation
# Full module names required (no PhoenixKitWeb alias in shop namespace)
scope "/", alias: false do
unquote(shop_live_routes)
end
end
end
end
# Generates all admin routes
defmacro phoenix_kit_admin_routes(suffix) do
session_name = :"phoenix_kit_admin#{suffix}"
# Auto-generate routes for custom admin tabs that specify live_view
# Skip when compiling PhoenixKit's own dev/test router — parent modules don't exist
custom_admin_routes = compile_custom_admin_routes(__CALLER__.module)
# Plugin module routes get their own live_session with admin layout
# so plugin LiveViews don't need to wrap with LayoutWrapper themselves
plugin_admin_routes = compile_plugin_admin_routes(__CALLER__.module)
# Get external route module AST outside quote to avoid require/alias inside quote
emails_admin = safe_route_call(EmailsRoutes, :admin_routes, [])
{tickets_admin, publishing_admin, referrals_admin} =
if suffix == :_locale do
{
safe_route_call(CustomerServiceRoutes, :admin_locale_routes, []),
safe_route_call(PublishingRoutes, :admin_locale_routes, []),
safe_route_call(ReferralsRoutes, :admin_locale_routes, [])
}
else
{
safe_route_call(CustomerServiceRoutes, :admin_routes, []),
safe_route_call(PublishingRoutes, :admin_routes, []),
safe_route_call(ReferralsRoutes, :admin_routes, [])
}
end
# External route modules with complex routes (beyond simple admin tabs)
external_admin_routes = compile_external_admin_routes(suffix)
quote do
live_session unquote(session_name),
on_mount: [{PhoenixKitWeb.Users.Auth, :phoenix_kit_ensure_admin}] do
# Core admin routes (under PhoenixKitWeb alias from parent scope)
live "/admin", Live.Dashboard, :index
live "/admin/users", Live.Users.Users, :index
live "/admin/users/new", Users.UserForm, :new, as: :user_form
live "/admin/users/edit/:id", Users.UserForm, :edit, as: :user_form_edit
live "/admin/users/view/:id", Live.Users.UserDetails, :show
live "/admin/users/roles", Live.Users.Roles, :index
live "/admin/users/permissions", Live.Users.PermissionsMatrix, :index
live "/admin/users/live_sessions", Live.Users.LiveSessions, :index
live "/admin/users/sessions", Live.Users.Sessions, :index
live "/admin/media", Live.Users.Media, :index
live "/admin/media/:file_uuid", Live.Users.MediaDetail, :show
live "/admin/media/selector", Live.Users.MediaSelector, :index
live "/admin/settings", Live.Settings, :index
live "/admin/settings/users", Live.Settings.Users, :index
live "/admin/settings/organization", Live.Settings.Organization, :index
live "/admin/modules", Live.Modules, :index
# Posts module routes
live "/admin/posts", Live.Modules.Posts.Posts, :index
live "/admin/posts/new", Live.Modules.Posts.Edit, :new
live "/admin/posts/groups", Live.Modules.Posts.Groups, :index
live "/admin/posts/groups/new", Live.Modules.Posts.GroupEdit, :new
live "/admin/posts/groups/:id/edit", Live.Modules.Posts.GroupEdit, :edit
live "/admin/posts/:id", Live.Modules.Posts.Details, :show
live "/admin/posts/:id/edit", Live.Modules.Posts.Edit, :edit
live "/admin/settings/posts", Live.Modules.Posts.Settings, :index
live "/admin/settings/languages", Live.Modules.Languages, :index
live "/admin/settings/languages/frontend", Live.Modules.Languages, :frontend
live "/admin/settings/languages/backend", Live.Modules.Languages, :backend
live "/admin/settings/legal", Live.Modules.Legal.Settings, :index
live "/admin/settings/maintenance", Live.Modules.Maintenance.Settings, :index
live "/admin/settings/seo", Live.Settings.SEO, :index
live "/admin/settings/media", Live.Modules.Storage.Settings, :index
live "/admin/settings/media/buckets/new", Live.Modules.Storage.BucketForm, :new
live "/admin/settings/media/buckets/:id/edit", Live.Modules.Storage.BucketForm, :edit
live "/admin/settings/media/dimensions", Live.Modules.Storage.Dimensions, :index
live "/admin/settings/media/dimensions/new/image",
Live.Modules.Storage.DimensionForm,
:new_image
live "/admin/settings/media/dimensions/new/video",
Live.Modules.Storage.DimensionForm,
:new_video
live "/admin/settings/media/dimensions/:id/edit",
Live.Modules.Storage.DimensionForm,
:edit
# Jobs
live "/admin/jobs", Live.Modules.Jobs.Index, :index
# Module admin routes (use alias: false to prevent PhoenixKitWeb prefix
# since these modules use their own namespaces like PhoenixKit.Modules.*)
scope "/", alias: false do
# Sitemap settings
live "/admin/settings/sitemap",
PhoenixKit.Modules.Sitemap.Web.Settings,
:index,
as: :sitemap_settings
# Billing admin routes
live "/admin/billing", PhoenixKit.Modules.Billing.Web.Index, :index, as: :billing_index
live "/admin/billing/orders", PhoenixKit.Modules.Billing.Web.Orders, :index,
as: :billing_orders
live "/admin/billing/orders/new", PhoenixKit.Modules.Billing.Web.OrderForm, :new,
as: :billing_order_new
live "/admin/billing/orders/:id", PhoenixKit.Modules.Billing.Web.OrderDetail, :show,
as: :billing_order_detail
live "/admin/billing/orders/:id/edit", PhoenixKit.Modules.Billing.Web.OrderForm, :edit,
as: :billing_order_edit
live "/admin/billing/invoices", PhoenixKit.Modules.Billing.Web.Invoices, :index,
as: :billing_invoices
live "/admin/billing/invoices/:id", PhoenixKit.Modules.Billing.Web.InvoiceDetail, :show,
as: :billing_invoice_detail
live "/admin/billing/invoices/:id/print",
PhoenixKit.Modules.Billing.Web.InvoicePrint,
:print,
as: :billing_invoice_print
live "/admin/billing/invoices/:id/receipt",
PhoenixKit.Modules.Billing.Web.ReceiptPrint,
:receipt,
as: :billing_receipt_print
live "/admin/billing/invoices/:id/credit-note/:transaction_uuid",
PhoenixKit.Modules.Billing.Web.CreditNotePrint,
:credit_note,
as: :billing_credit_note
live "/admin/billing/invoices/:id/payment/:transaction_uuid",
PhoenixKit.Modules.Billing.Web.PaymentConfirmationPrint,
:payment_confirmation,
as: :billing_payment_confirmation
live "/admin/billing/transactions", PhoenixKit.Modules.Billing.Web.Transactions, :index,
as: :billing_transactions
live "/admin/billing/subscriptions",
PhoenixKit.Modules.Billing.Web.Subscriptions,
:index,
as: :billing_subscriptions
live "/admin/billing/subscriptions/new",
PhoenixKit.Modules.Billing.Web.SubscriptionForm,
:new,
as: :billing_subscription_new
live "/admin/billing/subscriptions/:id",
PhoenixKit.Modules.Billing.Web.SubscriptionDetail,
:show,
as: :billing_subscription_detail
live "/admin/billing/subscription-types",
PhoenixKit.Modules.Billing.Web.SubscriptionTypes,
:index,
as: :billing_subscription_types
live "/admin/billing/subscription-types/new",
PhoenixKit.Modules.Billing.Web.SubscriptionTypeForm,
:new,
as: :billing_subscription_type_new
live "/admin/billing/subscription-types/:id/edit",
PhoenixKit.Modules.Billing.Web.SubscriptionTypeForm,
:edit,
as: :billing_subscription_type_edit
live "/admin/billing/profiles", PhoenixKit.Modules.Billing.Web.BillingProfiles, :index,
as: :billing_profiles
live "/admin/billing/profiles/new",
PhoenixKit.Modules.Billing.Web.BillingProfileForm,
:new,
as: :billing_profile_new
live "/admin/billing/profiles/:id/edit",
PhoenixKit.Modules.Billing.Web.BillingProfileForm,
:edit,
as: :billing_profile_edit
live "/admin/billing/currencies", PhoenixKit.Modules.Billing.Web.Currencies, :index,
as: :billing_currencies
live "/admin/settings/billing", PhoenixKit.Modules.Billing.Web.Settings, :settings,
as: :billing_settings
live "/admin/settings/billing/providers",
PhoenixKit.Modules.Billing.Web.ProviderSettings,
:index,
as: :billing_provider_settings
# DB Explorer routes
live "/admin/db", PhoenixKit.Modules.DB.Web.Index, :index, as: :db_index
live "/admin/db/activity", PhoenixKit.Modules.DB.Web.Activity, :activity,
as: :db_activity
live "/admin/db/:schema/:table", PhoenixKit.Modules.DB.Web.Show, :show, as: :db_show
# Comments module routes
live "/admin/comments", PhoenixKit.Modules.Comments.Web.Index, :index,
as: :comments_index
live "/admin/settings/comments", PhoenixKit.Modules.Comments.Web.Settings, :settings,
as: :comments_settings
# Sync module routes
live "/admin/sync", PhoenixKit.Modules.Sync.Web.Index, :index, as: :sync_index
live "/admin/sync/connections", PhoenixKit.Modules.Sync.Web.ConnectionsLive, :index,
as: :sync_connections
live "/admin/sync/history", PhoenixKit.Modules.Sync.Web.History, :index,
as: :sync_history
# Entities module routes
live "/admin/entities", PhoenixKit.Modules.Entities.Web.Entities, :index, as: :entities
live "/admin/entities/new", PhoenixKit.Modules.Entities.Web.EntityForm, :new,
as: :entities_new
live "/admin/entities/:id/edit", PhoenixKit.Modules.Entities.Web.EntityForm, :edit,
as: :entities_edit
live "/admin/entities/:entity_slug/data",
PhoenixKit.Modules.Entities.Web.DataNavigator,
:entity,
as: :entities_data_entity
live "/admin/entities/:entity_slug/data/new",
PhoenixKit.Modules.Entities.Web.DataForm,
:new,
as: :entities_data_new
live "/admin/entities/:entity_slug/data/:uuid",
PhoenixKit.Modules.Entities.Web.DataForm,
:show,
as: :entities_data_show
live "/admin/entities/:entity_slug/data/:uuid/edit",
PhoenixKit.Modules.Entities.Web.DataForm,
:edit,
as: :entities_data_edit
live "/admin/settings/entities",
PhoenixKit.Modules.Entities.Web.EntitiesSettings,
:index,
as: :entities_settings
# Shop admin routes
live "/admin/shop", PhoenixKit.Modules.Shop.Web.Dashboard, :index, as: :shop_dashboard
live "/admin/shop/products", PhoenixKit.Modules.Shop.Web.Products, :index,
as: :shop_products
live "/admin/shop/products/new", PhoenixKit.Modules.Shop.Web.ProductForm, :new,
as: :shop_product_new
live "/admin/shop/products/:id", PhoenixKit.Modules.Shop.Web.ProductDetail, :show,
as: :shop_product_detail
live "/admin/shop/products/:id/edit", PhoenixKit.Modules.Shop.Web.ProductForm, :edit,
as: :shop_product_edit
live "/admin/shop/categories", PhoenixKit.Modules.Shop.Web.Categories, :index,
as: :shop_categories
live "/admin/shop/categories/new", PhoenixKit.Modules.Shop.Web.CategoryForm, :new,
as: :shop_category_new
live "/admin/shop/categories/:id/edit", PhoenixKit.Modules.Shop.Web.CategoryForm, :edit,
as: :shop_category_edit
live "/admin/shop/shipping", PhoenixKit.Modules.Shop.Web.ShippingMethods, :index,
as: :shop_shipping_methods
live "/admin/shop/shipping/new", PhoenixKit.Modules.Shop.Web.ShippingMethodForm, :new,
as: :shop_shipping_new
live "/admin/shop/shipping/:id/edit",
PhoenixKit.Modules.Shop.Web.ShippingMethodForm,
:edit,
as: :shop_shipping_edit
live "/admin/shop/carts", PhoenixKit.Modules.Shop.Web.Carts, :index, as: :shop_carts
live "/admin/shop/settings", PhoenixKit.Modules.Shop.Web.Settings, :index,
as: :shop_settings
live "/admin/shop/settings/options",
PhoenixKit.Modules.Shop.Web.OptionsSettings,
:index,
as: :shop_options_settings
live "/admin/shop/settings/import-configs",
PhoenixKit.Modules.Shop.Web.ImportConfigs,
:index,
as: :shop_import_configs
live "/admin/shop/imports", PhoenixKit.Modules.Shop.Web.Imports, :index,
as: :shop_imports
live "/admin/shop/imports/:uuid", PhoenixKit.Modules.Shop.Web.ImportShow, :show,
as: :shop_import_show
live "/admin/shop/test", PhoenixKit.Modules.Shop.Web.TestShop, :index, as: :shop_test
# AI module routes
live "/admin/ai", PhoenixKit.Modules.AI.Web.Endpoints, :index, as: :ai_index
live "/admin/ai/endpoints", PhoenixKit.Modules.AI.Web.Endpoints, :endpoints,
as: :ai_endpoints
live "/admin/ai/usage", PhoenixKit.Modules.AI.Web.Endpoints, :usage, as: :ai_usage
live "/admin/ai/endpoints/new", PhoenixKit.Modules.AI.Web.EndpointForm, :new,
as: :ai_endpoint_new
live "/admin/ai/endpoints/:id/edit", PhoenixKit.Modules.AI.Web.EndpointForm, :edit,
as: :ai_endpoint_edit
live "/admin/ai/prompts", PhoenixKit.Modules.AI.Web.Prompts, :index, as: :ai_prompts
live "/admin/ai/prompts/new", PhoenixKit.Modules.AI.Web.PromptForm, :new,
as: :ai_prompt_new
live "/admin/ai/prompts/:id/edit", PhoenixKit.Modules.AI.Web.PromptForm, :edit,
as: :ai_prompt_edit
# Routes from external route modules
unquote(emails_admin)
unquote(tickets_admin)
unquote(publishing_admin)
unquote(referrals_admin)
# Custom admin routes from :admin_dashboard_tabs config
# Tabs with live_view: {Module, :action} get auto-generated routes
# in the shared admin live_session for seamless navigation
unquote_splicing(custom_admin_routes)
# External route modules (complex multi-page routes)
unquote_splicing(external_admin_routes)
# Plugin module routes (in same live_session for seamless navigation).
# Admin layout is auto-applied via on_mount for external plugin views.
unquote_splicing(plugin_admin_routes)
end
end
end
end
# Generates unified authenticated user routes: dashboard + shop user + tickets user.
# All routes share one live_session for seamless navigation within the user dashboard.
# Module routes use alias: false since they live outside the PhoenixKitWeb namespace.
defmacro phoenix_kit_authenticated_routes(suffix) do
session_name = :"phoenix_kit_authenticated#{suffix}"
module_routes =
if suffix == :_locale do
authenticated_live_locale_routes()
else
authenticated_live_routes()
end
quote do
live_session unquote(session_name),
on_mount: [
{PhoenixKitWeb.Users.Auth, :phoenix_kit_ensure_authenticated_scope},
{PhoenixKitWeb.Dashboard.ContextProvider, :default}
] do
# Core dashboard routes (conditional on config)
if unquote(PhoenixKit.Config.user_dashboard_enabled?()) do
live "/dashboard", Live.Dashboard.Index, :index
live "/dashboard/settings", Live.Dashboard.Settings, :edit
live "/dashboard/settings/confirm-email/:token",
Live.Dashboard.Settings,
:confirm_email
end
# Module user pages (full module names — no PhoenixKitWeb alias)
scope "/", alias: false do
unquote(module_routes)
end
end
end
end
defp authenticated_live_routes do
quote do
# Shop user pages
live "/dashboard/orders", PhoenixKit.Modules.Shop.Web.UserOrders, :index,
as: :shop_user_orders
live "/dashboard/orders/:uuid", PhoenixKit.Modules.Shop.Web.UserOrderDetails, :show,
as: :shop_user_order_details
live "/dashboard/billing-profiles",
PhoenixKit.Modules.Billing.Web.UserBillingProfiles,
:index,
as: :user_billing_profiles
live "/dashboard/billing-profiles/new",
PhoenixKit.Modules.Billing.Web.UserBillingProfileForm,
:new,
as: :user_billing_profile_new
live "/dashboard/billing-profiles/:id/edit",
PhoenixKit.Modules.Billing.Web.UserBillingProfileForm,
:edit,
as: :user_billing_profile_edit
# Tickets user pages
live "/dashboard/customer-service/tickets",
PhoenixKit.Modules.CustomerService.Web.UserList,
:index,
as: :tickets_user_list
live "/dashboard/customer-service/tickets/new",
PhoenixKit.Modules.CustomerService.Web.UserNew,
:new,
as: :tickets_user_new
live "/dashboard/customer-service/tickets/:id",
PhoenixKit.Modules.CustomerService.Web.UserDetails,
:show,
as: :tickets_user_details
end
end
defp authenticated_live_locale_routes do
quote do
# Shop user pages (locale variants — distinct aliases to avoid duplicate route names)
live "/dashboard/orders", PhoenixKit.Modules.Shop.Web.UserOrders, :index,
as: :shop_user_orders_locale
live "/dashboard/orders/:uuid", PhoenixKit.Modules.Shop.Web.UserOrderDetails, :show,
as: :shop_user_order_details_locale
live "/dashboard/billing-profiles",
PhoenixKit.Modules.Billing.Web.UserBillingProfiles,
:index,
as: :user_billing_profiles_locale
live "/dashboard/billing-profiles/new",
PhoenixKit.Modules.Billing.Web.UserBillingProfileForm,
:new,
as: :user_billing_profile_new_locale
live "/dashboard/billing-profiles/:id/edit",
PhoenixKit.Modules.Billing.Web.UserBillingProfileForm,
:edit,
as: :user_billing_profile_edit_locale
# Tickets user pages (locale variants)
live "/dashboard/customer-service/tickets",
PhoenixKit.Modules.CustomerService.Web.UserList,
:index,
as: :tickets_user_list_locale
live "/dashboard/customer-service/tickets/new",
PhoenixKit.Modules.CustomerService.Web.UserNew,
:new,
as: :tickets_user_new_locale
live "/dashboard/customer-service/tickets/:id",
PhoenixKit.Modules.CustomerService.Web.UserDetails,
:show,
as: :tickets_user_details_locale
end
end
# Generates user dashboard routes (conditional on config).
# @deprecated Use phoenix_kit_authenticated_routes/1 instead.
defmacro phoenix_kit_dashboard_routes(suffix) do
session_name = :"phoenix_kit_user_dashboard#{suffix}"
quote do
if unquote(PhoenixKit.Config.user_dashboard_enabled?()) do
live_session unquote(session_name),
on_mount: [
{PhoenixKitWeb.Users.Auth, :phoenix_kit_ensure_authenticated_scope},
{PhoenixKitWeb.Dashboard.ContextProvider, :default}
] do
live "/dashboard", Live.Dashboard.Index, :index
live "/dashboard/settings", Live.Dashboard.Settings, :edit
live "/dashboard/settings/confirm-email/:token",
Live.Dashboard.Settings,
:confirm_email
end
end
end
end
# Reads :admin_dashboard_tabs config at compile time and generates
# `live` route declarations for tabs that specify a `live_view` field.
# Returns a list of quoted expressions for use with unquote_splicing.
#
# ## Tab config example
#
# config :phoenix_kit, :admin_dashboard_tabs, [
# %{id: :admin_analytics, label: "Analytics", path: "/admin/analytics",
# live_view: {MyAppWeb.AnalyticsLive, :index}, permission: "dashboard"}
# ]
#
@doc false
def compile_custom_admin_routes(caller_module) do
# PhoenixKit's own router is only for dev/test — parent app modules
# aren't available when it compiles, so skip custom route generation.
if caller_module == PhoenixKitWeb.Router do
[]
else
compile_custom_admin_routes_internal()
end
end
@doc false
def compile_plugin_admin_routes(caller_module) do
if caller_module == PhoenixKitWeb.Router do
[]
else
compile_module_admin_routes()
end
end
defp compile_custom_admin_routes_internal do
case Application.get_env(:phoenix_kit, :admin_dashboard_tabs) do
tabs when is_list(tabs) ->
tabs
|> Enum.filter(fn tab ->
is_map(tab) and Map.has_key?(tab, :live_view) and
match?({module, _action} when is_atom(module), tab.live_view)
end)
|> Enum.map(&tab_to_route/1)
_ ->
[]
end
end
# Auto-discover admin routes from external PhoenixKit modules.
# Uses beam file scanning (same pattern as protocol consolidation) — zero config needed.
# Modules declare their admin tabs with live_view field for route auto-generation.
# Collects both admin_tabs and settings_tabs for complete route coverage.
defp compile_module_admin_routes do
PhoenixKit.ModuleDiscovery.discover_external_modules()
|> Enum.flat_map(fn mod ->
case Code.ensure_compiled(mod) do
{:module, _} ->
admin = collect_module_tabs(mod, :admin_tabs)
settings = collect_module_tabs(mod, :settings_tabs)
admin ++ settings
_ ->
[]
end
end)
end
defp collect_module_tabs(mod, callback) do
alias PhoenixKit.Dashboard.Tab
context = tab_callback_context(callback)
if function_exported?(mod, callback, 0) do
apply(mod, callback, [])
|> Enum.map(&Tab.resolve_path(&1, context))
|> Enum.filter(&tab_has_live_view?/1)
|> Enum.map(&tab_struct_to_route/1)
else
[]
end
end
defp tab_callback_context(:admin_tabs), do: :admin
defp tab_callback_context(:settings_tabs), do: :settings
defp tab_callback_context(:user_dashboard_tabs), do: :user_dashboard
defp tab_has_live_view?(%{live_view: {mod, _action}}) when is_atom(mod) do
case Code.ensure_compiled(mod) do
{:module, _} ->
true
{:error, reason} ->
IO.warn(
"[PhoenixKit] Tab references LiveView #{inspect(mod)} which failed to compile: " <>
"#{inspect(reason)}. Route will be skipped."
)
false
end
end
defp tab_has_live_view?(_), do: false
defp tab_struct_to_route(%{live_view: {module, action}, path: path, id: id}) do
route_opts = if id, do: [as: id], else: []
quote do
live unquote(path), unquote(module), unquote(action), unquote(route_opts)
end
end
defp tab_to_route(tab) do
{module, action} = tab.live_view
path = tab[:path] || raise "Tab #{tab[:id]} has :live_view but no :path"
route_opts = if tab[:id], do: [as: tab[:id]], else: []
quote do
live unquote(path), unquote(module), unquote(action), unquote(route_opts)
end
end
# Safely call a route module function at compile time.
# Returns empty AST if the module isn't available (allows safe extraction).
@doc false
def safe_route_call(mod, fun, args) do
case Code.ensure_compiled(mod) do
{:module, _} when is_atom(fun) ->
if function_exported?(mod, fun, length(args)),
do: apply(mod, fun, args),
else: quote(do: nil)
{:error, reason} ->
IO.warn(
"[PhoenixKit] Route module #{inspect(mod)} failed to compile: #{inspect(reason)}. " <>
"Its routes will be unavailable."
)
quote(do: nil)
end
end
# Compile admin routes from external route modules configured via:
# config :phoenix_kit, :route_modules, [MyApp.Routes.CustomRoutes]
# Each module should implement admin_routes/0 or admin_locale_routes/0
@doc false
def compile_external_admin_routes(suffix) do
fun = if suffix == :_locale, do: :admin_locale_routes, else: :admin_routes
Application.get_env(:phoenix_kit, :route_modules, [])
|> Enum.flat_map(&collect_admin_routes(&1, fun))
end
defp collect_admin_routes(mod, fun) do
case Code.ensure_compiled(mod) do
{:module, _} -> resolve_admin_routes(mod, fun)
_ -> []
end
end
defp resolve_admin_routes(mod, fun) do
cond do
function_exported?(mod, fun, 0) -> normalize_routes(apply(mod, fun, []))
function_exported?(mod, :admin_routes, 0) -> normalize_routes(mod.admin_routes())
true -> []
end
end
# Compile public routes from external route modules.
# Each module should implement public_routes/1 (receives url_prefix).
@doc false
def compile_external_public_routes(url_prefix) do
Application.get_env(:phoenix_kit, :route_modules, [])
|> Enum.flat_map(&collect_public_routes(&1, url_prefix))
end
defp collect_public_routes(mod, url_prefix) do
case Code.ensure_compiled(mod) do
{:module, _} ->
if function_exported?(mod, :public_routes, 1),
do: normalize_routes(mod.public_routes(url_prefix)),
else: []
_ ->
[]
end
end
defp normalize_routes(routes) when is_list(routes), do: routes
defp normalize_routes(route), do: [route]
# ============================================================================
# Route Scope Generators
# ============================================================================
# Helper function to generate localized routes
defp generate_localized_routes(url_prefix, pattern) do
quote do
# Localized scope: public routes (no plug-level auth check) + admin
# :phoenix_kit_shop_session is included so the cart session is available
# on all public pages; the plug is a no-op when Shop module is disabled.
scope "#{unquote(url_prefix)}/:locale", PhoenixKitWeb,
locale: ~r/^(#{unquote(pattern)})$/ do
pipe_through [
:browser,
:phoenix_kit_auto_setup,
:phoenix_kit_shop_session,
:phoenix_kit_locale_validation
]
# POST routes for authentication (needed for locale-prefixed form submissions)
post "/users/log-in", Users.Session, :create
delete "/users/log-out", Users.Session, :delete
get "/users/log-out", Users.Session, :get_logout
get "/users/magic-link/:token", Users.MagicLinkVerify, :verify
# OAuth routes
get "/users/auth/:provider", Users.OAuth, :request
get "/users/auth/:provider/callback", Users.OAuth, :callback
# Magic Link Registration
get "/users/register/verify/:token", Users.MagicLinkRegistrationVerify, :verify
phoenix_kit_public_routes(:_locale)
phoenix_kit_admin_routes(:_locale)
end
# Localized scope: authenticated user routes (plug-level auth check)
scope "#{unquote(url_prefix)}/:locale", PhoenixKitWeb,
locale: ~r/^(#{unquote(pattern)})$/ do
pipe_through [
:browser,
:phoenix_kit_auto_setup,
:phoenix_kit_require_authenticated,
:phoenix_kit_locale_validation
]
phoenix_kit_authenticated_routes(:_locale)
end
end
end
# Helper function to generate non-localized routes
defp generate_non_localized_routes(url_prefix) do
quote do
# Non-localized scope: public routes (no plug-level auth check) + admin
# :phoenix_kit_shop_session is included so the cart session is available
# on all public pages; the plug is a no-op when Shop module is disabled.
scope unquote(url_prefix), PhoenixKitWeb do
pipe_through [
:browser,
:phoenix_kit_auto_setup,
:phoenix_kit_shop_session,
:phoenix_kit_locale_validation
]
phoenix_kit_public_routes(:"")
phoenix_kit_admin_routes(:"")
end
# Non-localized scope: authenticated user routes (plug-level auth check)
scope unquote(url_prefix), PhoenixKitWeb do
pipe_through [
:browser,
:phoenix_kit_auto_setup,
:phoenix_kit_require_authenticated,
:phoenix_kit_locale_validation
]
phoenix_kit_authenticated_routes(:"")
end
end
end
defmacro phoenix_kit_routes do
# OAuth configuration is handled by PhoenixKit.Workers.OAuthConfigLoader
# which runs synchronously during supervisor startup
# No need for async spawn() here anymore
# Get URL prefix at compile time and handle empty string case for router compatibility
raw_prefix =
try do
PhoenixKit.Config.get_url_prefix()
rescue
# Fallback if config not available at compile time
_ -> "/phoenix_kit"
end
url_prefix =
case raw_prefix do
"" -> "/"
prefix -> prefix
end
# Use a generic locale pattern that accepts any valid language code format
# This allows switching to any of the 80+ predefined languages
# Actual validation of whether the locale is supported happens in the validation plug
pattern = "[a-z]{2}(?:-[A-Za-z0-9]{2,})?"
# Call route generators BEFORE quote block (aliases work in this context)
# Uses safe_route_call/3 so modules can be safely extracted to separate packages
emails_routes = safe_route_call(EmailsRoutes, :generate, [url_prefix])
publishing_routes = safe_route_call(PublishingRoutes, :generate, [url_prefix])
customer_service_routes = safe_route_call(CustomerServiceRoutes, :generate, [url_prefix])
blog_routes = safe_route_call(BlogRoutes, :generate, [url_prefix])
# External route modules with public/non-admin routes
external_public_routes = compile_external_public_routes(url_prefix)
quote do
# Generate pipeline definitions
unquote(generate_pipelines())
# Generate basic routes scope
unquote(generate_basic_scope(url_prefix))
# Generate module routes from separate files (improves compilation time)
unquote(emails_routes)
unquote(publishing_routes)
unquote(customer_service_routes)
# Generate localized routes
unquote(generate_localized_routes(url_prefix, pattern))
# Generate non-localized routes
unquote(generate_non_localized_routes(url_prefix))
# Generate blog routes (after other routes to prevent conflicts)
unquote(blog_routes)
# External route modules with public routes
unquote_splicing(external_public_routes)
# Generate catch-all route for pages at root level (must be last)
unquote(generate_pages_catch_all())
end
end
@doc """
**DEPRECATED**: This macro is no longer needed.
The Sync WebSocket is now automatically handled via the router when you
use `phoenix_kit_routes()`. The websocket endpoint is available at
`{url_prefix}/sync/websocket` without any additional configuration.
You can safely remove this macro from your endpoint.ex if you have it.
## Legacy Usage (deprecated)
Previously, this macro was required in endpoint.ex:
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
import PhoenixKitWeb.Integration
# No longer needed - remove this line
# phoenix_kit_socket()
end
## Implementation Note
WebSocket handling is now done via `forward` in the router, which makes
the setup fully self-contained within PhoenixKit. No endpoint modifications
are required.
"""
@deprecated "Sync websocket is now handled automatically via phoenix_kit_routes()"
defmacro phoenix_kit_socket do
quote do
plug PhoenixKit.Modules.Sync.Web.SocketPlug
end
end
def init(opts) do
opts
end
def call(conn, :phoenix_kit_auto_setup) do
# Add backward compatibility for layouts that use render_slot(@inner_block)
Plug.Conn.assign(conn, :inner_block, [])
end
end