Files
Beam/lib/forum/router.ex
2026-06-10 11:51:56 -05:00

126 lines
3.2 KiB
Elixir

defmodule Forum.Router do
use Plug.Router
use Joken.Config
require Logger
plug Plug.Logger
plug :log_request
plug :fetch_cookies
plug :verify_session_host
plug :match
plug :dispatch
defp verify_session_host(conn, _opts) do
if conn.host == "session.frm.so" do
case authenticate(conn) do
{:ok, claims} ->
conn
|> Plug.Conn.assign(:claims, claims)
:error ->
conn |> send_resp(401, "unauthorized") |> halt()
end
else
conn
end
end
defp authenticate(conn) do
with token when is_binary(token) <- conn.cookies["auth_token"], {:ok, claims} <- verify_token(token) do
{:ok, claims}
else
_ -> :error
end
end
defp verify_token(token) do
secret = Application.fetch_env!(:forum, :auth)[:jwt_secret]
signer = Joken.Signer.create("HS256", secret)
case Joken.verify_and_validate(%{}, token, signer) do
{:ok, claims} -> {:ok, claims}
{:error, _reason} = err -> err
end
end
get "/admin", host: "session.frm.so", do: handle_admin(conn)
get "/admin/graphyellow.svg", host: "session.frm.so", do: admin_asset(conn, "ui/graphyellow.svg", "image/svg+xml")
get "/admin/*path", host: "session.frm.so", do: handle_admin(conn)
get "/", host: "session.frm.so" do
user_id = conn.assigns.claims["id"]
email = conn.assigns.claims["email"]
conn
|> WebSockAdapter.upgrade(Forum.WsHandler, %{user_id: user_id, email: email}, timeout: :infinity)
|> halt()
end
match _ do
send_resp(conn, 404, "not found")
end
defp handle_admin(conn) do
if websocket_upgrade?(conn) do
conn
|> WebSockAdapter.upgrade(Forum.WsHandler, %{}, timeout: :infinity)
|> halt()
else
admin_page(conn)
end
end
defp websocket_upgrade?(conn) do
connection = get_req_header(conn, "connection") |> Enum.join(",") |> String.downcase()
upgrade = get_req_header(conn, "upgrade") |> List.first() |> to_string() |> String.downcase()
String.contains?(connection, "upgrade") and upgrade == "websocket"
end
defp admin_page(conn) do
html = File.read!(Forum.Assets.path("ui/admin.html"))
conn
|> put_resp_content_type("text/html")
|> send_resp(200, html)
end
defp admin_asset(conn, path, content_type) do
conn
|> put_resp_content_type(content_type)
|> send_file(200, Forum.Assets.path(path))
end
defp log_request(conn, _opts) do
started_at = System.monotonic_time(:microsecond)
Plug.Conn.register_before_send(conn, fn conn ->
duration_us = System.monotonic_time(:microsecond) - started_at
Forum.LogStore.add(%{
"source_ip" => remote_ip(conn),
"host" => conn.host || "",
"method" => conn.method,
"path" => conn.request_path,
"query_string" => conn.query_string,
"status" => conn.status || 0,
"duration_ms" => Float.round(duration_us / 1_000, 2)
})
conn
end)
end
defp remote_ip(conn) do
case Plug.Conn.get_req_header(conn, "x-real-ip") do
[ip | _] -> ip
[] -> fallback_ip(conn)
end
end
defp fallback_ip(%{remote_ip: remote_ip}) when is_tuple(remote_ip) do
remote_ip |> :inet.ntoa() |> to_string()
end
defp fallback_ip(_), do: ""
end