defmodule ToweropsWeb.Router do @moduledoc false use ToweropsWeb, :router use Honeybadger.Plug use ErrorTracker.Web, :router import Oban.Web.Router import Phoenix.LiveDashboard.Router import ToweropsWeb.UserAuth alias ToweropsWeb.Api.V1 alias ToweropsWeb.Plugs.RateLimit pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash plug :put_root_layout, html: {ToweropsWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers, %{ # Content-Security-Policy for LiveView applications # Note: 'unsafe-inline' for scripts is required for LiveView to function # WebSocket connection required for LiveView real-time updates "content-security-policy" => "default-src 'self'; " <> "script-src 'self' 'unsafe-inline'; " <> "style-src 'self' 'unsafe-inline'; " <> "img-src 'self' data: https:; " <> "font-src 'self' data:; " <> "connect-src 'self' ws: wss:; " <> "frame-ancestors 'none';" } plug ToweropsWeb.Plugs.CaptureTimezone plug :fetch_current_scope_for_user plug :set_user_locale plug ToweropsWeb.Plugs.UpdateSessionActivity plug :store_return_to_for_liveview plug ToweropsWeb.Plugs.DetectEUUser plug ToweropsWeb.Plugs.CheckPolicyConsent end pipeline :api do plug :accepts, ["json"] end pipeline :mobile_api do plug :accepts, ["json"] plug ToweropsWeb.Plugs.MobileAuth end pipeline :api_v1 do plug :accepts, ["json"] plug ToweropsWeb.Plugs.ApiAuth end # Rate limiting pipelines pipeline :rate_limit_auth do plug RateLimit, type: :auth end pipeline :rate_limit_api do plug RateLimit, type: :api end pipeline :graphql_context do plug ToweropsWeb.Plugs.GraphQLIntrospection plug ToweropsWeb.GraphQL.Context end # Health check endpoint for Kubernetes probes (no authentication required) scope "/", ToweropsWeb do get "/health", HealthController, :index end scope "/", ToweropsWeb do pipe_through :browser get "/", PageController, :home get "/privacy", PageController, :privacy get "/terms", PageController, :terms get "/docs/api", ApiDocsController, :index get "/docs/graphql", GraphQLDocsController, :index get "/invitations/:token", InvitationController, :show live "/help", HelpLive.Index, :index end # Account API routes (requires browser authentication) scope "/api/v1/account", ToweropsWeb.Api do pipe_through [:browser, :require_authenticated_user] get "/data", AccountDataController, :show end # Agent communication via WebSocket channel at /socket/agent # See ToweropsWeb.AgentChannel for implementation # Mobile Auth API routes (no authentication required for login flow) # Rate limited to prevent brute-force attacks on QR token verification scope "/api/v1/mobile/auth", ToweropsWeb.Api do pipe_through [:api, :rate_limit_auth] post "/qr/verify", MobileAuthController, :verify_qr_token post "/qr/complete", MobileAuthController, :complete_qr_login end # Mobile API routes (requires mobile authentication) # Rate limited to prevent API abuse scope "/api/v1/mobile", ToweropsWeb.Api do pipe_through [:mobile_api, :rate_limit_api] # Auth session management get "/auth/session", MobileAuthController, :get_session delete "/auth/session", MobileAuthController, :revoke_session # Data endpoints get "/organizations", MobileController, :list_organizations get "/organizations/:organization_id/sites", MobileController, :list_sites get "/organizations/:organization_id/devices", MobileController, :list_devices get "/organizations/:organization_id/alerts", MobileController, :list_alerts get "/devices/:id", MobileController, :get_equipment end # Stripe webhook endpoint (no authentication, verified by signature) scope "/api/v1", V1 do pipe_through :api post "/webhooks/stripe", StripeWebhookController, :create end # GraphQL API endpoint (requires API token authentication) scope "/api/graphql" do pipe_through [:api_v1, :rate_limit_api, :graphql_context] forward "/", Absinthe.Plug, schema: ToweropsWeb.GraphQL.Schema, analyze_complexity: true, max_complexity: 500, max_depth: 10 end # API v1 routes (requires API token authentication) # Rate limited to prevent API abuse scope "/api/v1", V1 do pipe_through [:api_v1, :rate_limit_api] resources "/sites", SitesController, except: [:new, :edit] resources "/devices", DevicesController, except: [:new, :edit] resources "/agents", AgentsController, except: [:new, :edit] resources "/alerts", AlertsController, only: [:index, :show] post "/alerts/:id/acknowledge", AlertsController, :acknowledge post "/alerts/:id/resolve", AlertsController, :resolve get "/organization", OrganizationController, :show patch "/organization", OrganizationController, :update resources "/members", MembersController, only: [:index, :delete, :update] resources "/invitations", InvitationsController, only: [:index, :create, :delete] resources "/integrations", IntegrationsController, except: [:new, :edit] post "/integrations/:id/test", IntegrationsController, :test_connection get "/devices/:device_id/checks", CheckResultsController, :checks get "/devices/:device_id/metrics", CheckResultsController, :metrics get "/devices/:device_id/interfaces", CheckResultsController, :interfaces get "/activity", ActivityController, :index end # Webhook routes (shared secret authentication) pipeline :webhook do plug :accepts, ["json"] plug ToweropsWeb.Plugs.WebhookAuth end scope "/api/v1/webhooks", V1 do pipe_through :webhook post "/agent-release", AgentReleaseWebhookController, :create end # Gaiia webhook routes (per-organization secret verification in controller) scope "/api/v1/webhooks", V1 do pipe_through [:api, :rate_limit_api] post "/gaiia/:organization_id", GaiiaWebhookController, :create post "/pagerduty/:organization_id", PagerdutyWebhookController, :create end # Admin API routes (requires superuser API token) scope "/admin/api", V1 do pipe_through :api_v1 # MIB file management get "/mibs", MibController, :index post "/mibs", MibController, :upload delete "/mibs/:vendor", MibController, :delete # GeoIP database management post "/geoip/import", GeoipController, :import_database end # Enable LiveDashboard in production with authentication scope "/admin" do pipe_through [:browser, :require_authenticated_user, :require_superuser] live_dashboard "/dashboard", metrics: ToweropsWeb.Telemetry, ecto_repos: [Towerops.Repo], ecto_psql_extras_options: [ # Exclude TimescaleDB internal tables from constraint checks missing_fk_constraints: [ exclude: ~r/^_(hyper_|materialized_hypertable_)/ ] ] end # Enable Oban Web (superuser only) scope "/admin" do pipe_through [:browser, :require_authenticated_user, :require_superuser] oban_dashboard("/oban") error_tracker_dashboard("/errors") get "/headers", ToweropsWeb.DebugController, :headers end # Enable Swoosh mailbox preview in development if Application.compile_env(:towerops, :dev_routes) do scope "/dev" do pipe_through :browser forward "/mailbox", Plug.Swoosh.MailboxPreview end end ## Authentication routes ## Rate limited to prevent brute-force attacks scope "/", ToweropsWeb do pipe_through [:browser, :redirect_if_user_is_authenticated, :rate_limit_auth] live "/users/register", UserRegistrationLive, :new end scope "/", ToweropsWeb do pipe_through [:browser, :require_authenticated_user] get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email end scope "/", ToweropsWeb do pipe_through [:browser, :rate_limit_auth] get "/users/log-in", UserSessionController, :new get "/users/log-in/totp", UserSessionController, :totp_verification_form post "/users/log-in/totp", UserSessionController, :verify_totp get "/users/log-in/:token", UserSessionController, :confirm post "/users/log-in", UserSessionController, :create delete "/users/log-out", UserSessionController, :delete get "/users/confirm/:token", UserConfirmationController, :confirm get "/users/confirm", UserConfirmationController, :new post "/users/confirm", UserConfirmationController, :create get "/users/reset-password", UserResetPasswordController, :new post "/users/reset-password", UserResetPasswordController, :create live "/users/reset-password/:token", UserResetPasswordLive, :edit end # Sudo mode verification routes scope "/", ToweropsWeb do pipe_through [:browser, :require_authenticated_user] get "/users/sudo-verify", UserSudoController, :new post "/users/sudo-verify", UserSudoController, :verify post "/orgs/switch", UserSessionController, :switch_org end ## Admin routes (superuser only) pipeline :admin_rate_limit do plug RateLimit, type: :admin end scope "/", ToweropsWeb do pipe_through [:browser, :require_authenticated_user, :require_superuser, :admin_rate_limit] post "/admin/impersonate/:user_id", AdminController, :start_impersonate delete "/admin/impersonate", AdminController, :stop_impersonate post "/admin/oban/flush", AdminController, :flush_oban end live_session :require_superuser, on_mount: [ {ToweropsWeb.UserAuth, :load_cookie_consent}, {ToweropsWeb.UserAuth, :require_authenticated_user}, {ToweropsWeb.UserAuth, :require_totp_enrollment}, {ToweropsWeb.UserAuth, :require_superuser}, {ToweropsWeb.UserAuth, :handle_policy_consent} ] do scope "/admin", ToweropsWeb.Admin do pipe_through [:browser, :require_authenticated_user, :require_superuser, :admin_rate_limit] live "/", DashboardLive, :index live "/users", UserLive.Index, :index live "/organizations", OrgLive.Index, :index live "/audit", AuditLive.Index, :index live "/monitoring", MonitoringLive, :index live "/agents", AgentLive.Index, :index live "/security", SecurityLive.Index, :index end end ## Organization routes live_session :require_sudo_mode, on_mount: [ {ToweropsWeb.UserAuth, :load_cookie_consent}, {ToweropsWeb.UserAuth, :require_authenticated_user}, {ToweropsWeb.UserAuth, :require_totp_enrollment}, {ToweropsWeb.UserAuth, :require_sudo_mode}, {ToweropsWeb.UserAuth, :handle_policy_consent} ] do scope "/", ToweropsWeb do pipe_through [:browser, :require_authenticated_user] live "/users/settings", UserSettingsLive, :index live "/users/my-data", AccountLive.MyData, :index live "/account/activity", AccountLive.Activity, :index end end # TOTP enrollment page - requires auth but NOT TOTP (since user is enrolling) live_session :totp_enrollment, on_mount: [ {ToweropsWeb.UserAuth, :load_cookie_consent}, {ToweropsWeb.UserAuth, :require_authenticated_user} ] do scope "/", ToweropsWeb do pipe_through [:browser, :require_authenticated_user] live "/account/totp-enrollment", AccountLive.TotpEnrollment, :index end end live_session :require_authenticated_user, on_mount: [ {ToweropsWeb.UserAuth, :load_cookie_consent}, {ToweropsWeb.UserAuth, :require_authenticated_user}, {ToweropsWeb.UserAuth, :require_totp_enrollment}, {ToweropsWeb.UserAuth, :handle_policy_consent} ] do scope "/", ToweropsWeb do pipe_through [:browser, :require_authenticated_user] live "/mobile/qr-login", MobileQRLive, :index live "/orgs", OrgLive.Index, :index live "/orgs/new", OrgLive.New, :new end end live_session :require_authenticated_user_and_organization, on_mount: [ {ToweropsWeb.UserAuth, :load_cookie_consent}, {ToweropsWeb.UserAuth, :require_authenticated_user}, {ToweropsWeb.UserAuth, :require_totp_enrollment}, {ToweropsWeb.UserAuth, :load_current_organization}, {ToweropsWeb.UserAuth, :handle_policy_consent} ] do scope "/orgs/:org_slug", ToweropsWeb do pipe_through [:browser, :require_authenticated_user, :load_current_organization] live "/settings", Org.SettingsLive, :index live "/settings/integrations", Org.IntegrationsLive, :index live "/settings/integrations/preseem/devices", Org.PreseemDevicesLive, :index live "/settings/integrations/preseem/insights", Org.PreseemInsightsLive, :index live "/settings/integrations/gaiia/mapping", Org.GaiiaMappingLive, :index live "/settings/integrations/gaiia/reconciliation", Org.GaiiaReconciliationLive, :index end end live_session :require_authenticated_user_with_default_org, on_mount: [ {ToweropsWeb.UserAuth, :load_cookie_consent}, {ToweropsWeb.UserAuth, :require_authenticated_user}, {ToweropsWeb.UserAuth, :require_totp_enrollment}, {ToweropsWeb.UserAuth, :load_default_organization}, {ToweropsWeb.UserAuth, :handle_policy_consent} ] do scope "/", ToweropsWeb do pipe_through [:browser, :require_authenticated_user] # Dashboard route live "/dashboard", DashboardLive, :index # Alert routes live "/alerts", AlertLive.Index, :index # Agent routes live "/agents", AgentLive.Index, :index live "/agents/:id/edit", AgentLive.Edit, :edit live "/agents/:id", AgentLive.Show, :show # Site routes live "/sites", SiteLive.Index, :index live "/sites/new", SiteLive.Form, :new live "/sites/:id", SiteLive.Show, :show live "/sites/:id/edit", SiteLive.Form, :edit # Device routes live "/devices", DeviceLive.Index, :index live "/devices/new", DeviceLive.Form, :new live "/devices/:id", DeviceLive.Show, :show live "/devices/:id/edit", DeviceLive.Form, :edit live "/devices/:id/graph/check", GraphLive.Show, :show live "/devices/:id/graph/:sensor_type", GraphLive.Show, :show live "/devices/:device_id/backups/compare", MikrotikBackupLive.Compare, :compare live "/devices/:device_id/config-timeline", ConfigTimelineLive, :index # Insights route live "/insights", InsightsLive.Index, :index # Subscriber trace live "/trace", TraceLive.Index, :index # Activity feed live "/activity", ActivityFeedLive, :index # Maintenance windows live "/maintenance", MaintenanceLive.Index, :index live "/maintenance/new", MaintenanceLive.Form, :new live "/maintenance/:id", MaintenanceLive.Show, :show live "/maintenance/:id/edit", MaintenanceLive.Form, :edit # Changelog live "/changelog", ChangelogLive, :index # Network map routes live "/network-map", NetworkMapLive, :index live "/sites-map", MapLive.Index, :index end end end