Files

169 lines
4.3 KiB
Elixir

defmodule MoundHuntersWeb.Router do
@moduledoc """
Main HTTP router for Hopewell Road application.
"""
use Plug.Router
require Logger
# Request logging (must be first to capture timing)
plug(MoundHuntersWeb.Plugs.RequestLogger)
# Parse query params and request body
plug(Plug.Parsers,
parsers: [:urlencoded, :json],
json_decoder: Jason
)
# Bounds checking for Ohio coordinates
plug(MoundHuntersWeb.Plugs.BoundsCheck)
# Match routes
plug(:match)
# Dispatch to matched route
plug(:dispatch)
# Tile endpoints
get "/tiles/request" do
MoundHuntersWeb.TileController.request(conn)
end
match "/tiles/:format/:tile_id", via: [:get, :head] do
conn = put_private(conn, :path_params, %{"tile_id" => tile_id, "format" => format})
MoundHuntersWeb.TileController.serve(conn)
end
# Geometry sharing API
post "/api/share" do
MoundHuntersWeb.ApiController.create_share(conn)
end
get "/api/share/:id" do
conn = put_private(conn, :path_params, %{"id" => id})
MoundHuntersWeb.ApiController.get_share(conn)
end
# Tile metadata API
get "/api/meta/tile/:id" do
conn = put_private(conn, :path_params, %{"id" => id})
MoundHuntersWeb.ApiController.get_tile(conn)
end
get "/api/meta/tile" do
MoundHuntersWeb.ApiController.get_tile_by_coords(conn)
end
# Health check
get "/health" do
send_resp(conn, 200, Jason.encode!(%{status: "ok"}))
end
get "/" do
static_dir = Application.app_dir(:mound_hunters, "priv/static")
index_path = Path.join(static_dir, "index.html")
if File.exists?(index_path) do
conn
|> put_resp_header("content-type", "text/html; charset=utf-8")
|> put_resp_header("cache-control", "no-cache, must-revalidate")
|> send_file(200, index_path)
else
send_resp(conn, 404, "Not found")
end
end
# Static files (with cache busting support via manifest)
get "/*path" do
serve_static(conn, path)
end
# 404 handler
match _ do
send_resp(conn, 404, Jason.encode!(%{error: "Not found"}))
end
# Serve static files from priv/static
defp serve_static(conn, path) do
static_dir = Application.app_dir(:mound_hunters, "priv/static")
file_path = resolve_static_path(path, static_dir)
cond do
file_path && File.regular?(file_path) ->
# Determine content type
content_type = MIME.from_path(file_path)
# Set cache headers (immutable for hashed files)
cache_control =
if String.contains?(file_path, "-") do
"public, max-age=31536000, immutable"
else
"public, max-age=3600"
end
conn
|> put_resp_header("content-type", content_type)
|> put_resp_header("cache-control", cache_control)
|> send_file(200, file_path)
path == [] or path == [""] ->
# Serve index.html for root
index_path = Path.join(static_dir, "index.html")
if File.exists?(index_path) do
conn
|> put_resp_header("content-type", "text/html")
|> put_resp_header("cache-control", "no-cache")
|> send_file(200, index_path)
else
send_resp(conn, 404, "Not found")
end
true ->
send_resp(conn, 404, "Not found")
end
end
# Resolve static file path, checking manifest for hashed versions
defp resolve_static_path(path, static_dir) do
requested = Path.join(path)
direct_path = Path.join(static_dir, requested)
cond do
File.regular?(direct_path) ->
direct_path
# Check manifest for hashed version
manifest_path = Path.join(static_dir, "manifest.json") ->
case load_manifest(manifest_path) do
{:ok, manifest} ->
hashed = Map.get(manifest, requested)
if hashed do
hashed_path = Path.join(static_dir, hashed)
if File.regular?(hashed_path), do: hashed_path, else: nil
else
nil
end
_ ->
nil
end
true ->
nil
end
end
# Load and cache manifest
defp load_manifest(path) do
if File.exists?(path) do
case File.read(path) do
{:ok, content} -> Jason.decode(content)
_ -> {:error, :read_failed}
end
else
{:error, :not_found}
end
end
end