528 lines
19 KiB
Elixir
528 lines
19 KiB
Elixir
defmodule ToweropsWeb.Router do
|
|
@moduledoc false
|
|
|
|
use ToweropsWeb, :router
|
|
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
|
|
|
|
# CSP and most security headers are handled by ToweropsWeb.Plugs.SecurityHeaders
|
|
# (endpoint-level, single source of truth). Only pass through the default
|
|
# put_secure_browser_headers options here for headers SecurityHeaders does not set
|
|
# (x-download-options, x-permitted-cross-domain-policies, cross-origin-opener-policy).
|
|
plug :put_secure_browser_headers
|
|
|
|
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_api do
|
|
plug :accepts, ["json"]
|
|
plug ToweropsWeb.Plugs.GraphQLAuth
|
|
end
|
|
|
|
pipeline :graphql_context do
|
|
plug ToweropsWeb.Plugs.GraphQLIntrospection
|
|
plug ToweropsWeb.GraphQL.Context
|
|
end
|
|
|
|
# Health check endpoints for Kubernetes probes (no authentication required).
|
|
# /health is a deep check (db + redis) used for readiness/startup; /health/live
|
|
# is a shallow liveness check that must not depend on external services, so a
|
|
# transient db/redis blip can't make k8s kill an otherwise-healthy pod.
|
|
scope "/", ToweropsWeb do
|
|
get "/health", HealthController, :index, log: false
|
|
get "/health/live", HealthController, :live, log: false
|
|
end
|
|
|
|
# Public crawlable endpoints (no pipeline, no CSRF/session required)
|
|
scope "/", ToweropsWeb do
|
|
get "/sitemap.xml", SitemapController, :index
|
|
end
|
|
|
|
# Well-known discovery endpoints (no pipeline, public access, no auth/CSRF)
|
|
scope "/.well-known", ToweropsWeb do
|
|
get "/api-catalog", WellKnownController, :api_catalog
|
|
get "/openid-configuration", WellKnownController, :openid_configuration
|
|
get "/oauth-authorization-server", WellKnownController, :oauth_authorization_server
|
|
get "/oauth-protected-resource", WellKnownController, :oauth_protected_resource
|
|
get "/agent-skills/index.json", WellKnownController, :agent_skills
|
|
end
|
|
|
|
scope "/.well-known/mcp", ToweropsWeb do
|
|
get "/server-card.json", WellKnownController, :mcp_server_card
|
|
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
|
|
|
|
# Public status page (unauthenticated)
|
|
live "/status/:slug", StatusPageLive, :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 (accepts API tokens or mobile session tokens)
|
|
scope "/api/graphql" do
|
|
pipe_through [:graphql_api, :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 "/checks", ChecksController, 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
|
|
|
|
# RF coverage prediction (Terraform-friendly resourceful CRUD).
|
|
resources "/coverages", CoveragesController, except: [:new, :edit]
|
|
post "/coverages/:id/recompute", CoveragesController, :recompute
|
|
get "/coverages/:id/kmz", CoveragesController, :kmz
|
|
|
|
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
|
|
|
|
# On-call schedules
|
|
resources "/schedules", SchedulesController, except: [:new, :edit]
|
|
get "/schedules/:id/on_call", SchedulesController, :on_call
|
|
post "/schedules/:id/layers", SchedulesController, :create_layer
|
|
patch "/schedules/:id/layers/:layer_id", SchedulesController, :update_layer
|
|
delete "/schedules/:id/layers/:layer_id", SchedulesController, :delete_layer
|
|
post "/schedules/:id/layers/:layer_id/members", SchedulesController, :create_member
|
|
delete "/schedules/:id/layers/:layer_id/members/:member_id", SchedulesController, :delete_member
|
|
post "/schedules/:id/overrides", SchedulesController, :create_override
|
|
delete "/schedules/:id/overrides/:override_id", SchedulesController, :delete_override
|
|
|
|
# Escalation policies
|
|
resources "/escalation_policies", EscalationPoliciesController, except: [:new, :edit]
|
|
post "/escalation_policies/:id/rules", EscalationPoliciesController, :create_rule
|
|
patch "/escalation_policies/:id/rules/:rule_id", EscalationPoliciesController, :update_rule
|
|
delete "/escalation_policies/:id/rules/:rule_id", EscalationPoliciesController, :delete_rule
|
|
post "/escalation_policies/:id/rules/:rule_id/targets", EscalationPoliciesController, :create_target
|
|
delete "/escalation_policies/:id/rules/:rule_id/targets/:target_id", EscalationPoliciesController, :delete_target
|
|
|
|
# Maintenance windows
|
|
resources "/maintenance_windows", MaintenanceWindowsController, except: [:new, :edit]
|
|
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
|
|
post "/mikrotik/:organization_id/dhcp", MikrotikWebhookController, :dhcp
|
|
post "/mikrotik/:organization_id/pppoe", MikrotikWebhookController, :pppoe
|
|
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")
|
|
end
|
|
|
|
# Enable Swoosh mailbox preview and debug endpoints in development only
|
|
if Application.compile_env(:towerops, :dev_routes) do
|
|
scope "/admin" do
|
|
pipe_through [:browser, :require_authenticated_user, :require_superuser]
|
|
get "/headers", ToweropsWeb.DebugController, :headers
|
|
end
|
|
|
|
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, :subscribe_to_status_updates},
|
|
{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, :subscribe_to_status_updates},
|
|
{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
|
|
|
|
# RF Link Health
|
|
live "/rf-links", RfLinkHealthLive, :index
|
|
|
|
# Reports
|
|
live "/reports", ReportsLive, :index
|
|
|
|
# Subscriber trace
|
|
live "/trace", TraceLive.Index, :index
|
|
|
|
# Activity feed
|
|
live "/activity", ActivityFeedLive, :index
|
|
|
|
# On-call schedules
|
|
live "/schedules", ScheduleLive.Index, :index
|
|
live "/schedules/new", ScheduleLive.Form, :new
|
|
live "/schedules/escalation-policies", EscalationPolicyLive.Index, :index
|
|
live "/schedules/escalation-policies/new", EscalationPolicyLive.Form, :new
|
|
live "/schedules/escalation-policies/:id", EscalationPolicyLive.Show, :show
|
|
live "/schedules/escalation-policies/:id/edit", EscalationPolicyLive.Form, :edit
|
|
live "/schedules/:id", ScheduleLive.Show, :show
|
|
live "/schedules/:id/edit", ScheduleLive.Form, :edit
|
|
|
|
# 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 "/weathermap", WeathermapLive, :index
|
|
live "/sites-map", MapLive.Index, :index
|
|
|
|
# RF coverage prediction
|
|
live "/coverage", CoverageLive.Map, :index
|
|
live "/coverage/list", CoverageLive.Index, :index
|
|
live "/coverage/new", CoverageLive.Form, :new
|
|
live "/coverage/:id", CoverageLive.Show, :show
|
|
live "/coverage/:id/edit", CoverageLive.Form, :edit
|
|
end
|
|
end
|
|
end
|