towerops/lib/towerops_web/router.ex
Graham McIntire f6e9063577
feat: add REST API and GraphQL for on-call schedules and escalation policies
REST API v1 endpoints for schedules (14 endpoints) and escalation
policies (10 endpoints) with full CRUD for nested resources (layers,
members, overrides, rules, targets). GraphQL types, resolvers, and
schema mutations for both resource trees.
2026-03-11 14:01:15 -05:00

481 lines
17 KiB
Elixir

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
# 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
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, :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
# Capacity route
live "/capacity", CapacityLive, :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
# 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 "/sites-map", MapLive.Index, :index
end
end
end