Files
wanderer/lib/wanderer_app_web/router.ex
2025-09-07 21:15:35 +02:00

611 lines
19 KiB
Elixir

defmodule WandererAppWeb.Router do
use WandererAppWeb, :router
use ErrorTracker.Web, :router
use Plug.ErrorHandler
import PlugDynamic.Builder
import WandererAppWeb.UserAuth,
warn: false,
only: [redirect_if_user_is_authenticated: 2]
import WandererAppWeb.BasicAuth,
warn: false,
only: [admin_basic_auth: 2]
# import WandererAppWeb.Plugs.LicenseAuth,
# warn: false,
# only: [authenticate_lm: 2, authenticate_license: 2]
@code_reloading Application.compile_env(
:wanderer_app,
[WandererAppWeb.Endpoint, :code_reloader],
false
)
@frame_src_values if(@code_reloading, do: ["'self'"], else: [])
# Define style sources individually to ensure proper spacing
@style_src_values [
"'self'",
"'unsafe-inline'",
"https://fonts.googleapis.com",
"https://cdn.jsdelivr.net/npm/",
"https://cdnjs.cloudflare.com/ajax/libs/"
]
# Define image sources individually to ensure proper spacing
@img_src_values [
"'self'",
"data:",
"https://images.evetech.net",
"https://web.ccpgamescdn.com",
"https://images.ctfassets.net",
"https://w.appzi.io"
]
# Define font sources individually to ensure proper spacing
@font_src_values [
"'self'",
"https://fonts.gstatic.com",
"data:",
"https://web.ccpgamescdn.com",
"https://w.appzi.io"
]
# Define script sources individually to ensure proper spacing
@script_src_values [
"'self'",
"'unsafe-inline'",
"https://cdn.jsdelivr.net/npm/",
"https://cdnjs.cloudflare.com/ajax/libs/",
"https://unpkg.com",
"https://cdn.jsdelivr.net",
"https://w.appzi.io",
"https://www.googletagmanager.com",
"https://cdnjs.cloudflare.com"
]
# Define connect sources individually to ensure proper spacing
@connect_src_values [
"'self'",
"https://api.appzi.io",
"https://www.googletagmanager.com",
"https://www.google-analytics.com",
"https://*.google-analytics.com"
]
# Define sandbox values individually to ensure proper spacing
@sandbox_values [
"allow-forms",
"allow-scripts",
"allow-modals",
"allow-same-origin",
"allow-downloads",
"allow-popups"
]
pipeline :admin_bauth do
plug :admin_basic_auth
end
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {WandererAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
dynamic_plug PlugContentSecurityPolicy, reevaluate: :first_usage do
URI.default_port("wss", 443)
URI.default_port("ws", 80)
home_url = URI.parse(WandererAppWeb.Endpoint.url())
ws_url =
home_url
|> Map.update!(:scheme, fn
"http" -> "ws"
"https" -> "wss"
end)
|> Map.put(:path, "")
|> URI.to_string()
# Get the HTTP URL from home_url
http_url = URI.to_string(home_url)
# Only add script-src-elem when in development mode
script_src_elem =
if(@code_reloading,
do: @script_src_values ++ [ws_url, http_url],
else: @script_src_values
)
directives = %{
default_src: ~w('none'),
script_src: @script_src_values ++ [ws_url],
style_src: @style_src_values,
img_src: @img_src_values,
font_src: @font_src_values,
connect_src: @connect_src_values ++ [ws_url],
media_src: ~w('none'),
object_src: ~w('none'),
child_src: ~w('none'),
frame_src: @frame_src_values,
worker_src: ~w('none'),
frame_ancestors: ~w('none'),
form_action: ~w('self'),
block_all_mixed_content: ~w(),
sandbox: @sandbox_values,
base_uri: ~w('none'),
manifest_src: ~w('self')
}
# Only add script-src-elem to directives when in development mode
directives = Map.put(directives, :script_src_elem, script_src_elem)
directives =
case home_url do
%URI{scheme: "http"} -> directives
%URI{scheme: "https"} -> Map.put(directives, :upgrade_insecure_requests, ~w())
end
[
directives: directives
]
end
plug WandererAppWeb.Plugs.SetUser
end
pipeline :blog do
plug :put_layout, html: {WandererAppWeb.Layouts, :blog}
end
pipeline :api do
plug WandererAppWeb.Plugs.ContentNegotiation, accepts: ["json"]
plug :accepts, ["json"]
plug WandererAppWeb.Plugs.CheckApiDisabled
end
# Versioned API pipeline with enhanced security and validation
pipeline :api_versioned do
plug WandererAppWeb.Plugs.ContentNegotiation, accepts: ["json"]
plug :accepts, ["json"]
plug WandererAppWeb.Plugs.CheckApiDisabled
plug WandererAppWeb.Plugs.RequestValidator
plug WandererAppWeb.Plugs.ApiVersioning
plug WandererAppWeb.Plugs.ResponseSanitizer
end
pipeline :api_map do
plug WandererAppWeb.Plugs.CheckMapApiKey
plug WandererAppWeb.Plugs.CheckMapSubscription
plug WandererAppWeb.Plugs.AssignMapOwner
end
pipeline :api_sse do
plug WandererAppWeb.Plugs.CheckApiDisabled
plug WandererAppWeb.Plugs.CheckSseDisabled
plug WandererAppWeb.Plugs.CheckMapApiKey
plug WandererAppWeb.Plugs.CheckMapSubscription
plug WandererAppWeb.Plugs.AssignMapOwner
end
pipeline :api_kills do
plug WandererAppWeb.Plugs.CheckApiDisabled
end
pipeline :api_character do
plug WandererAppWeb.Plugs.CheckCharacterApiDisabled
end
pipeline :api_websocket_events do
plug WandererAppWeb.Plugs.CheckWebsocketDisabled
end
pipeline :api_acl do
plug WandererAppWeb.Plugs.CheckAclApiKey
end
pipeline :api_spec do
plug OpenApiSpex.Plug.PutApiSpec,
otp_app: :wanderer_app,
module: WandererAppWeb.ApiSpec
end
pipeline :api_spec_v1 do
plug OpenApiSpex.Plug.PutApiSpec,
otp_app: :wanderer_app,
module: WandererAppWeb.OpenApiV1Spec
end
pipeline :api_spec_combined do
plug OpenApiSpex.Plug.PutApiSpec,
otp_app: :wanderer_app,
module: WandererAppWeb.ApiSpecV1
end
# New v1 API pipeline for ash_json_api
pipeline :api_v1 do
plug WandererAppWeb.Plugs.ContentNegotiation, accepts: ["json"]
plug :accepts, ["json", "json-api"]
plug :fetch_session
plug WandererAppWeb.Plugs.CheckApiDisabled
plug WandererAppWeb.Plugs.JsonApiPerformanceMonitor
plug WandererAppWeb.Plugs.CheckJsonApiAuth
# Future: Add rate limiting, advanced permissions, etc.
end
# pipeline :api_license_management do
# plug :authenticate_lm
# end
# pipeline :api_license_validation do
# plug :authenticate_license
# end
scope "/api/map/systems-kills", WandererAppWeb do
pipe_through [:api, :api_map, :api_kills]
get "/", MapAPIController, :list_systems_kills
end
scope "/api/map", WandererAppWeb do
pipe_through [:api, :api_map]
get "/audit", MapAuditAPIController, :index
# Deprecated routes - use /api/maps/:map_identifier/systems instead
get "/systems", MapSystemAPIController, :list_systems
get "/system", MapSystemAPIController, :show_system
get "/connections", MapConnectionAPIController, :list_all_connections
get "/characters", MapAPIController, :list_tracked_characters
get "/structure-timers", MapAPIController, :show_structure_timers
get "/character-activity", MapAPIController, :character_activity
get "/user_characters", MapAPIController, :user_characters
get "/acls", MapAccessListAPIController, :index
post "/acls", MapAccessListAPIController, :create
end
#
# SSE endpoint for real-time events (uses separate pipeline without accepts restriction)
#
scope "/api/maps/:map_identifier", WandererAppWeb do
pipe_through [:api_sse]
get "/events/stream", Api.EventsController, :stream
end
#
# Unified RESTful routes for systems & connections by slug or ID
#
scope "/api/maps/:map_identifier", WandererAppWeb do
pipe_through [:api, :api_map]
# Map duplication endpoint
post "/duplicate", MapAPIController, :duplicate_map
patch "/connections", MapConnectionAPIController, :update
delete "/connections", MapConnectionAPIController, :delete
delete "/systems", MapSystemAPIController, :delete
resources "/systems", MapSystemAPIController, only: [:index, :show, :create, :update, :delete]
resources "/connections", MapConnectionAPIController,
only: [:index, :show, :create, :update, :delete],
param: "id"
resources "/structures", MapSystemStructureAPIController, except: [:new, :edit]
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
resources "/signatures", MapSystemSignatureAPIController, except: [:new, :edit]
get "/user-characters", MapAPIController, :show_user_characters
get "/tracked-characters", MapAPIController, :show_tracked_characters
end
# WebSocket events and webhook management endpoints (disabled by default)
scope "/api/maps/:map_identifier", WandererAppWeb do
pipe_through [:api, :api_map, :api_websocket_events]
get "/events", MapEventsAPIController, :list_events
# Webhook management endpoints
resources "/webhooks", MapWebhooksAPIController, except: [:new, :edit] do
post "/rotate-secret", MapWebhooksAPIController, :rotate_secret
end
# Webhook control endpoint
put "/webhooks/toggle", MapAPIController, :toggle_webhooks
end
#
# Other API routes
#
scope "/api/characters", WandererAppWeb do
pipe_through [:api, :api_character]
get "/", CharactersAPIController, :index
end
scope "/api/acls", WandererAppWeb do
pipe_through [:api, :api_acl]
get "/:id", MapAccessListAPIController, :show
put "/:id", MapAccessListAPIController, :update
post "/:acl_id/members", AccessListMemberAPIController, :create
put "/:acl_id/members/:member_id", AccessListMemberAPIController, :update_role
delete "/:acl_id/members/:member_id", AccessListMemberAPIController, :delete
end
scope "/api/common", WandererAppWeb do
pipe_through [:api]
get "/system-static-info", CommonAPIController, :show_system_static
end
scope "/api" do
pipe_through [:api_spec]
get "/openapi", OpenApiSpex.Plug.RenderSpec, :show
end
# Combined spec needs its own pipeline
scope "/api" do
pipe_through [:api_spec_combined]
get "/openapi-complete", OpenApiSpex.Plug.RenderSpec, :show
end
scope "/api/v1" do
pipe_through [:api_spec_v1]
# v1 JSON:API spec (bypasses authentication)
get "/open_api", OpenApiSpex.Plug.RenderSpec, :show
end
#
# Health Check Endpoints
# Used for monitoring, load balancer health checks, and deployment validation
#
scope "/api", WandererAppWeb do
pipe_through [:api]
# Basic health check for load balancers (lightweight)
get "/health", Api.HealthController, :health
# Detailed health status for monitoring systems
get "/health/status", Api.HealthController, :status
# Readiness check for deployment validation
get "/health/ready", Api.HealthController, :ready
# Liveness check for container orchestration
get "/health/live", Api.HealthController, :live
# Metrics endpoint for monitoring systems
get "/health/metrics", Api.HealthController, :metrics
# Deep health check for comprehensive diagnostics
get "/health/deep", Api.HealthController, :deep
end
# scope "/api/licenses", WandererAppWeb do
# pipe_through [:api, :api_license_management]
# post "/", LicenseApiController, :create
# put "/:id/validity", LicenseApiController, :update_validity
# put "/:id/expiration", LicenseApiController, :update_expiration
# get "/map/:map_id", LicenseApiController, :get_by_map_id
# end
# scope "/api/license", WandererAppWeb do
# pipe_through [:api, :api_license_validation]
# get "/validate", LicenseApiController, :validate
# end
#
# Browser / blog stuff
#
scope "/", WandererAppWeb do
pipe_through [:browser, :blog, :redirect_if_user_is_authenticated]
get "/welcome", BlogController, :index
end
scope "/contacts", WandererAppWeb do
pipe_through [:browser, :blog]
get "/", BlogController, :contacts
end
scope "/changelog", WandererAppWeb do
pipe_through [:browser, :blog]
get "/", BlogController, :changelog
end
scope "/news", WandererAppWeb do
pipe_through [:browser, :blog]
get "/:slug", BlogController, :show
get "/", BlogController, :list
end
scope "/license", WandererAppWeb do
pipe_through [:browser, :blog]
get "/", BlogController, :license
end
scope "/swaggerui" do
pipe_through [:browser, :api_spec]
# v1 JSON:API (AshJsonApi generated)
get "/v1", OpenApiSpex.Plug.SwaggerUI,
path: "/api/v1/open_api",
title: "WandererApp v1 JSON:API Docs",
css_urls: [
# Standard Swagger UI CSS
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui.min.css",
# Material theme from swagger-ui-themes (v3.x):
"https://cdn.jsdelivr.net/npm/swagger-ui-themes@3.0.0/themes/3.x/theme-material.css"
],
js_urls: [
# We need both main JS & standalone preset for full styling
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui-bundle.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui-standalone-preset.min.js"
],
swagger_ui_config: %{
"docExpansion" => "none",
"deepLinking" => true,
"tagsSorter" => "alpha",
"operationsSorter" => "alpha"
}
# Legacy API only
get "/legacy", OpenApiSpex.Plug.SwaggerUI,
path: "/api/openapi",
title: "WandererApp Legacy API Docs",
css_urls: [
# Standard Swagger UI CSS
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui.min.css",
# Material theme from swagger-ui-themes (v3.x):
"https://cdn.jsdelivr.net/npm/swagger-ui-themes@3.0.0/themes/3.x/theme-material.css"
],
js_urls: [
# We need both main JS & standalone preset for full styling
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui-bundle.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui-standalone-preset.min.js"
],
swagger_ui_config: %{
"docExpansion" => "none",
"deepLinking" => true
}
# Complete API (Legacy + v1)
get "/", OpenApiSpex.Plug.SwaggerUI,
path: "/api/openapi-complete",
title: "WandererApp Complete API Docs (Legacy & v1)",
css_urls: [
# Standard Swagger UI CSS
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui.min.css",
# Material theme from swagger-ui-themes (v3.x):
"https://cdn.jsdelivr.net/npm/swagger-ui-themes@3.0.0/themes/3.x/theme-material.css"
],
js_urls: [
# We need both main JS & standalone preset for full styling
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui-bundle.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.5.0/swagger-ui-standalone-preset.min.js"
],
swagger_ui_config: %{
"docExpansion" => "none",
"deepLinking" => true,
"tagsSorter" => "alpha",
"operationsSorter" => "alpha"
}
end
#
# Auth
#
scope "/auth", WandererAppWeb do
pipe_through :browser
get "/signout", AuthController, :signout
get "/:provider", AuthController, :request
get "/:provider/callback", AuthController, :callback
end
#
# Admin
#
scope "/admin", WandererAppWeb do
pipe_through(:browser)
pipe_through(:admin_bauth)
live_session :admin,
on_mount: [
{WandererAppWeb.UserAuth, :ensure_authenticated},
{WandererAppWeb.UserAuth, :ensure_admin},
WandererAppWeb.Nav
] do
live("/", AdminLive, :index)
live("/invite", AdminLive, :add_invite_link)
end
error_tracker_dashboard("/errors",
on_mount: [
{WandererAppWeb.UserAuth, :ensure_authenticated},
{WandererAppWeb.UserAuth, :ensure_admin}
]
)
end
#
# Additional routes / Live sessions
#
scope "/", WandererAppWeb do
pipe_through(:browser)
get "/", RedirectController, :redirect_authenticated
get "/last", MapsController, :last
live_session :authenticated,
on_mount: [
{WandererAppWeb.UserAuth, :ensure_authenticated},
WandererAppWeb.Nav
] do
live "/access-lists/new", AccessListsLive, :create
live "/access-lists/:id/edit", AccessListsLive, :edit
live "/access-lists/:id/add-members", AccessListsLive, :add_members
live "/access-lists/:id", AccessListsLive, :members
live "/access-lists", AccessListsLive, :index
live "/coming-soon", ComingLive, :index
live "/tracking/:slug", CharactersTrackingLive, :characters
live "/tracking", CharactersTrackingLive, :index
live "/characters", CharactersLive, :index
live "/characters/authorize", CharactersLive, :authorize
live "/maps/new", MapsLive, :create
live "/maps/:slug/edit", MapsLive, :edit
live "/maps/:slug/settings", MapsLive, :settings
live "/maps", MapsLive, :index
live "/profile", ProfileLive, :index
live "/profile/deposit", ProfileLive, :deposit
live "/profile/subscribe", ProfileLive, :subscribe
live "/:slug/audit", MapAuditLive, :index
live "/:slug/characters", MapCharactersLive, :index
live "/:slug", MapLive, :index
end
end
if Application.compile_env(:wanderer_app, :dev_routes) do
import Phoenix.LiveDashboard.Router
scope "/dev" do
pipe_through(:browser)
error_tracker_dashboard("/errors", as: :error_tracker_dev_dashboard)
live_dashboard("/dashboard", metrics: WandererAppWeb.Telemetry)
end
end
#
# Versioned API Routes with backward compatibility
# These routes handle version negotiation and provide enhanced features per version
# Note: These are experimental routes for testing the versioning system
#
scope "/api/versioned" do
pipe_through :api_versioned
# Version-aware routes handled by ApiRouter
forward "/", WandererAppWeb.ApiRouter
end
#
# JSON:API v1 Routes (ash_json_api)
# These routes provide a modern JSON:API compliant interface
# while maintaining 100% backward compatibility with existing /api/* routes
#
scope "/api/v1" do
pipe_through :api_v1
# Custom combined endpoints
get "/maps/:map_id/systems_and_connections",
WandererAppWeb.Api.MapSystemsConnectionsController,
:show
# Forward all v1 requests to AshJsonApi router
# This will automatically generate RESTful JSON:API endpoints
# for all Ash resources once they're configured with the AshJsonApi extension
forward "/", WandererAppWeb.ApiV1Router
end
end