defmodule BerrypodWeb.Router do use BerrypodWeb, :router import BerrypodWeb.UserAuth import Phoenix.LiveDashboard.Router import ErrorTracker.Web.Router import Oban.Web.Router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash plug :put_root_layout, html: {BerrypodWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers plug :fetch_current_scope_for_user plug BerrypodWeb.Plugs.CountryDetect plug BerrypodWeb.Plugs.LoadTheme end pipeline :api do plug :accepts, ["json"] end # Lightweight pipeline for SVG recoloring — no session, CSRF, auth, or layout pipeline :image do plug :put_secure_browser_headers end # Minimal pipeline for robots.txt and sitemap.xml pipeline :seo do plug :put_secure_browser_headers end pipeline :printify_webhook do plug BerrypodWeb.Plugs.VerifyPrintifyWebhook end pipeline :printful_webhook do plug BerrypodWeb.Plugs.VerifyPrintfulWebhook end pipeline :shop do plug :put_root_layout, html: {BerrypodWeb.Layouts, :shop_root} plug BerrypodWeb.Plugs.Analytics end pipeline :admin do plug :put_root_layout, html: {BerrypodWeb.Layouts, :admin_root} end # ── Routes without the :browser pipeline ────────────────────────── # Health check (no auth, no theme loading — for load balancers and uptime monitors) scope "/", BerrypodWeb do pipe_through [:api] get "/health", HealthController, :show end # SEO — crawlers need these without any session/auth overhead scope "/", BerrypodWeb do pipe_through [:seo] get "/robots.txt", SeoController, :robots get "/sitemap.xml", SeoController, :sitemap end # Favicon & PWA manifest — served from DB, minimal pipeline scope "/", BerrypodWeb do pipe_through [:seo] get "/favicon.svg", FaviconController, :favicon_svg get "/favicon-32x32.png", FaviconController, :favicon_32 get "/apple-touch-icon.png", FaviconController, :apple_touch_icon get "/icon-192.png", FaviconController, :icon_192 get "/icon-512.png", FaviconController, :icon_512 get "/site.webmanifest", FaviconController, :webmanifest end # SVG recoloring (dynamic — can't be pre-generated to disk) scope "/images", BerrypodWeb do pipe_through :image get "/:id/recolored/:color", ImageController, :recolored_svg end # Webhook endpoints (no CSRF, signature verified) scope "/webhooks", BerrypodWeb do pipe_through [:api, :printify_webhook] post "/printify", WebhookController, :printify end scope "/webhooks", BerrypodWeb do pipe_through [:api, :printful_webhook] post "/printful", WebhookController, :printful end scope "/webhooks", BerrypodWeb do pipe_through [:api] post "/stripe", StripeWebhookController, :handle end # ── Routes with the :browser pipeline ───────────────────────────── # All routes below use :browser. The shop scope with its /:slug # catch-all MUST be last so it doesn't intercept other routes. # Setup page — minimal live_session, no theme/cart/search hooks scope "/", BerrypodWeb do pipe_through [:browser] get "/setup/login/:token", SetupController, :login get "/recover/login/:token", SetupController, :recover_login live_session :setup, on_mount: [{BerrypodWeb.UserAuth, :mount_current_scope}] do live "/setup", Setup.Onboarding, :index live "/recover", Setup.Recover, :index end end # LiveDashboard, ErrorTracker, and Oban Web behind admin auth scope "/admin" do pipe_through [:browser, :require_authenticated_user] live_dashboard "/dashboard", metrics: BerrypodWeb.Telemetry error_tracker_dashboard("/errors") oban_dashboard("/oban", resolver: BerrypodWeb.ObanResolver) end # Admin pages with sidebar layout scope "/admin", BerrypodWeb do pipe_through [:browser, :require_authenticated_user, :admin] get "/analytics/export", AnalyticsExportController, :export get "/newsletter/export", NewsletterExportController, :export # No-JS fallbacks for settings forms post "/settings/email", EmailSettingsController, :update post "/settings/email/test", EmailSettingsController, :test post "/settings/from-address", SettingsController, :update_from_address post "/settings/stripe/signing-secret", SettingsController, :update_signing_secret # Account TOTP routes (session-based for mobile reconnect persistence) post "/account/totp/start", AccountController, :start_totp_setup post "/account/totp/cancel", AccountController, :cancel_totp_setup get "/account/totp/complete", AccountController, :complete_totp_setup post "/account/totp/dismiss-codes", AccountController, :clear_backup_codes post "/navigation", NavigationController, :save post "/providers", ProvidersController, :create post "/providers/:id", ProvidersController, :update live_session :admin, layout: {BerrypodWeb.Layouts, :admin}, on_mount: [ {BerrypodWeb.UserAuth, :require_authenticated}, {BerrypodWeb.AdminLayoutHook, :assign_current_path} ] do live "/", Admin.Dashboard, :index live "/analytics", Admin.Analytics, :index live "/orders", Admin.Orders, :index live "/orders/:id", Admin.OrderShow, :show live "/activity", Admin.Activity, :index live "/products", Admin.Products, :index live "/products/:id", Admin.ProductShow, :show live "/providers", Admin.Providers.Index, :index live "/providers/new", Admin.Providers.Form, :new live "/providers/:id/edit", Admin.Providers.Form, :edit live "/settings", Admin.Settings, :index live "/settings/email", Admin.EmailSettings, :index live "/account", Admin.Account, :index live "/pages", Admin.Pages.Index, :index live "/pages/new", Admin.Pages.CustomForm, :new live "/pages/:slug/settings", Admin.Pages.CustomForm, :edit live "/pages/:slug", Admin.Pages.Editor, :edit live "/navigation", Admin.Navigation, :index live "/media", Admin.Media, :index live "/newsletter", Admin.Newsletter, :index live "/newsletter/campaigns/new", Admin.Newsletter.CampaignForm, :new live "/newsletter/campaigns/:id", Admin.Newsletter.CampaignForm, :edit live "/redirects", Admin.Redirects, :index end # Theme editor: admin root layout but full-screen (no sidebar) live_session :admin_theme, on_mount: [{BerrypodWeb.UserAuth, :require_authenticated}] do live "/theme", Admin.Theme.Index, :index end end # User account settings scope "/", BerrypodWeb do pipe_through [:browser, :require_authenticated_user] live_session :user_settings, on_mount: [{BerrypodWeb.UserAuth, :require_authenticated}] do live "/users/settings", Auth.Settings, :edit live "/users/settings/confirm-email/:token", Auth.Settings, :confirm_email end post "/users/update-password", UserSessionController, :update_password end scope "/", BerrypodWeb do pipe_through [:browser] live_session :current_user, on_mount: [ {BerrypodWeb.SetupHook, :require_admin}, {BerrypodWeb.UserAuth, :mount_current_scope} ] do live "/users/register", Auth.Registration, :new live "/users/log-in", Auth.Login, :new live "/users/log-in/:token", Auth.Confirmation, :new live "/users/totp", Auth.TotpVerification, :new end post "/users/log-in", UserSessionController, :create post "/users/verify-totp", UserSessionController, :verify_totp delete "/users/log-out", UserSessionController, :delete end # Order lookup verification — sets session email then redirects to /orders scope "/", BerrypodWeb do pipe_through [:browser] get "/orders/verify/:token", OrderLookupController, :verify get "/unsubscribe/:token", UnsubscribeController, :unsubscribe get "/newsletter/confirm/:token", NewsletterController, :confirm end # Dev-only routes (mailbox preview, error previews) if Application.compile_env(:berrypod, :dev_routes) do scope "/dev" do pipe_through :browser forward "/mailbox", Plug.Swoosh.MailboxPreview # Preview error pages get "/errors/404", BerrypodWeb.ErrorPreviewController, :not_found get "/errors/500", BerrypodWeb.ErrorPreviewController, :server_error end end # Cart API (session persistence for LiveView) scope "/api", BerrypodWeb do pipe_through [:browser] post "/cart", CartController, :update end # Public storefront — MUST be last because /:slug catch-all absorbs # any single-segment path not matched above scope "/", BerrypodWeb do pipe_through [:browser, :shop] live_session :coming_soon, layout: {BerrypodWeb.Layouts, :shop}, on_mount: [ {BerrypodWeb.SetupHook, :require_admin}, {BerrypodWeb.ThemeHook, :mount_theme}, {BerrypodWeb.AnalyticsHook, :track} ] do live "/coming-soon", Shop.ComingSoon, :index end live_session :public_shop, layout: {BerrypodWeb.Layouts, :shop}, on_mount: [ {BerrypodWeb.SetupHook, :require_admin}, {BerrypodWeb.UserAuth, :mount_current_scope}, {BerrypodWeb.ThemeHook, :mount_theme}, {BerrypodWeb.ThemeHook, :require_site_live}, {BerrypodWeb.CartHook, :mount_cart}, {BerrypodWeb.SearchHook, :mount_search}, {BerrypodWeb.AnalyticsHook, :track}, {BerrypodWeb.PageEditorHook, :mount_page_editor}, {BerrypodWeb.NewsletterHook, :mount_newsletter} ] do live "/", Shop.Page, :home live "/about", Shop.Page, :about live "/delivery", Shop.Page, :delivery live "/privacy", Shop.Page, :privacy live "/terms", Shop.Page, :terms live "/contact", Shop.Page, :contact live "/collections/:slug", Shop.Page, :collection live "/products/:id", Shop.Page, :product live "/cart", Shop.Page, :cart live "/search", Shop.Page, :search live "/checkout/success", Shop.Page, :checkout_success live "/orders", Shop.Page, :orders live "/orders/:order_number", Shop.Page, :order_detail # Catch-all for custom CMS pages — must be last live "/:slug", Shop.Page, :custom_page end # Checkout (POST — creates Stripe session and redirects) post "/checkout", CheckoutController, :create # Contact form + order lookup (no-JS fallbacks for contact page forms) post "/contact/send", ContactController, :create post "/contact/lookup", OrderLookupController, :lookup # Newsletter signup (no-JS fallback) post "/newsletter/subscribe", NewsletterController, :subscribe # Cart form actions (no-JS fallbacks for LiveView cart events) post "/cart/add", CartController, :add post "/cart/remove", CartController, :remove post "/cart/update", CartController, :update_item post "/cart/country", CartController, :update_country end end