tarakan/lib/tarakan_web/router.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

296 lines
12 KiB
Elixir

defmodule TarakanWeb.Router do
use TarakanWeb, :router
import TarakanWeb.AccountAuth
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {TarakanWeb.Layouts, :root}
plug TarakanWeb.CurrentPath
plug :protect_from_forgery
plug :put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; base-uri 'self'; frame-ancestors 'self'; " <>
"object-src 'none'; img-src 'self' data: https:; " <>
"font-src 'self' data:; style-src 'self' 'unsafe-inline'; " <>
"script-src 'self'; connect-src 'self' wss: ws:; form-action 'self'",
"permissions-policy" => "camera=(), microphone=(), geolocation=()",
"x-frame-options" => "SAMEORIGIN"
}
plug TarakanWeb.Plugs.CodeBrowserHeaders
# CodeBrowserRateLimit runs after the session scope is fetched so
# admins/moderators are exempt from the per-IP budget.
plug :fetch_current_scope_for_account
plug TarakanWeb.Plugs.CodeBrowserRateLimit
end
# OAuth starts/callbacks each force an upstream authorization redirect or
# token-exchange POST; keep them on a small per-IP budget so a flood
# cannot proxy abuse to GitHub/GitLab.
pipeline :oauth_rate_limited do
plug :rate_limit_oauth_ip
end
pipeline :api do
plug :accepts, ["json"]
plug TarakanWeb.Plugs.ApiRateLimit, mode: :ip
end
# Stripe webhooks: JSON in, no session/CSRF (signature-verified instead).
pipeline :stripe_webhook do
plug :accepts, ["json"]
end
scope "/webhooks", TarakanWeb.Webhooks do
pipe_through :stripe_webhook
post "/stripe", StripeController, :handle
end
pipeline :api_authenticated do
plug :fetch_api_account
plug TarakanWeb.Plugs.ApiRateLimit, mode: :actor
end
pipeline :seo do
plug :accepts, ["xml", "txt"]
end
# Badges are embedded in READMEs on other hosts: no session, no CSRF, no
# browser security headers, and an IP budget because anyone can hotlink one.
pipeline :badge do
plug :accepts, ["svg", "html"]
# Its own bucket: a badge embedded in a popular README is high-volume and
# cheap (cached), and must not drain the API budget for the same network.
plug TarakanWeb.Plugs.ApiRateLimit, mode: :ip, bucket: :badge_ip, request_limit: 600
end
scope "/", TarakanWeb do
pipe_through :seo
get "/robots.txt", SEOController, :robots
get "/.well-known/security.txt", SEOController, :security_txt
get "/feeds/findings.xml", FeedController, :findings
get "/feeds/infestations.xml", FeedController, :infestations
get "/sitemap.xml", SEOController, :sitemap
get "/sitemap/hubs.xml", SEOController, :sitemap_hubs
get "/sitemap/repos/:page", SEOController, :sitemap_repos
get "/sitemap/findings/:page", SEOController, :sitemap_findings
get "/sitemap/jobs/:page", SEOController, :sitemap_jobs
end
scope "/", TarakanWeb do
pipe_through [:browser, :oauth_rate_limited]
get "/auth/github", GitHubAuthController, :request
get "/auth/github/callback", GitHubAuthController, :callback
get "/auth/gitlab", GitLabAuthController, :request
get "/auth/gitlab/callback", GitLabAuthController, :callback
end
scope "/", TarakanWeb do
pipe_through :browser
# Permanent redirects from the pre-rename /patterns URLs.
get "/patterns", InfestationRedirectController, :index
get "/patterns/:pattern_key", InfestationRedirectController, :show
live_session :public,
on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
live "/", RepositoryLive.Index, :index
live "/policies/disclosure", PageLive, :disclosure
live "/policies/content", PageLive, :content
live "/pricing", PageLive, :pricing
live "/services/disclosure", PageLive, :managed_disclosure
live "/explore", ExploreLive, :index
live "/infestations", InfestationLive.Index, :index
live "/infestations/:pattern_key", InfestationLive.Show, :show
live "/leaderboard", LeaderboardLive, :index
live "/models", ModelAnalyticsLive, :index
live "/jobs", JobsLive, :index
live "/jobs/:id", ReviewTaskLive.Show, :show
live "/agents", AgentsLive, :index
live "/bounties", BountyLive.Index, :index
# /bounties/new authenticates in-mount (it must precede the :public_id
# route, and the public session runs first for the other two).
live "/bounties/new", BountyLive.New, :new
live "/bounties/:public_id", BountyLive.Show, :show
live "/findings/:public_id", FindingLive.Show, :show
live "/findings/:finding_ref/code", RepositoryCodeLive, :finding
end
end
scope "/badges", TarakanWeb do
pipe_through :badge
# Hosted repositories are addressed without a host, remote ones with it.
get "/:owner/:name", BadgeController, :show
get "/:host/:owner/:name", BadgeController, :show
end
scope "/api", TarakanWeb.API do
pipe_through [:api]
post "/client-auth/start", ClientAuthController, :start
post "/client-auth/exchange", ClientAuthController, :exchange
# Public, read-only record (IP rate-limited; no token required)
get "/findings/:public_id", FindingController, :show
get "/infestations", InfestationController, :index
get "/infestations/:pattern_key", InfestationController, :show
get "/infestations/:pattern_key/vaccine.yaml", InfestationController, :vaccine
get "/leaderboard", LeaderboardController, :index
end
scope "/api", TarakanWeb.API do
pipe_through [:api, :api_authenticated]
get "/repositories", RepositoryController, :index
post "/repositories", RepositoryController, :create
delete "/client-auth/session", ClientAuthController, :revoke
# Detectors. Submitting one requires a worker that validated it against
# known instances; reading one is free and lives on the public scope.
post "/patterns/:code_pattern_key/rule", InfestationController, :put_rule
# Jobs
get "/jobs", WorkController, :queue
get "/jobs/:id", WorkController, :show
post "/jobs/:id/claim", WorkController, :claim
post "/jobs/:id/claim/renew", WorkController, :renew
delete "/jobs/:id/claim", WorkController, :release
post "/jobs/:id/complete", WorkController, :complete
get "/:host/:owner/:name/jobs", WorkController, :index
# Reports + findings + checks
get "/:host/:owner/:name/reports", ScanController, :index
get "/:host/:owner/:name/memory", ScanController, :memory
post "/:host/:owner/:name/reports", ScanController, :create
post "/:host/:owner/:name/findings/:public_id/check", ScanController, :finding_verdict
post "/:host/:owner/:name/findings/:public_id/severity", ScanController, :finding_severity
post "/:host/:owner/:name/findings/:public_id/embedding", ScanController, :finding_embedding
post "/:host/:owner/:name/reports/:id/check", ScanController, :verdict
end
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:tarakan, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
import Phoenix.LiveDashboard.Router
scope "/dev" do
pipe_through :browser
live_dashboard "/dashboard", metrics: TarakanWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
## Authentication routes
scope "/", TarakanWeb do
pipe_through [:browser, :require_authenticated_account]
live_session :require_authenticated_account,
on_mount: [{TarakanWeb.AccountAuth, :require_authenticated}, TarakanWeb.CurrentPath] do
live "/accounts/settings", AccountLive.Settings, :edit
live "/accounts/settings/confirm-email/:token", AccountLive.Settings, :confirm_email
live "/accounts/settings/:section", AccountLive.Settings, :edit
live "/accounts/billing", BillingLive, :show
live "/alerts", AlertLive, :index
live "/client/authorize/:user_code", ClientAuthorizationLive, :show
live "/admin", AdminLive.Index, :index
live "/admin/accounts/:id", AdminLive.Show, :show
live "/repositories/new", RepositoryLive.New, :new
live "/moderation/report", ModerationReportLive.New, :new
live "/moderation/cases/:id", ModerationCaseLive.Show, :show
live "/moderation/queue", ModerationQueueLive.Index, :index
end
# Checkout/portal exit points: plain POSTs that 303 to Stripe.
post "/billing/checkout", BillingController, :checkout
post "/billing/portal", BillingController, :portal
post "/accounts/update-password", AccountSessionController, :update_password
end
scope "/", TarakanWeb do
pipe_through [:browser]
live_session :current_account,
on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
live "/accounts/register", AccountLive.Registration, :new
live "/accounts/log-in", AccountLive.Login, :new
live "/accounts/log-in/:token", AccountLive.Confirmation, :new
end
post "/accounts/log-in", AccountSessionController, :create
delete "/accounts/log-out", AccountSessionController, :delete
end
## Repository browsing
#
# Tarakan-hosted repositories live at GitHub-style bare paths
# (/handle/name); remote repositories lead with their host's domain
# (/github.com/owner/name, with legacy /github/... still resolving).
#
# Soundness: handles that would shadow a fixed route are reserved at
# registration (Tarakan.Accounts.Account), handles can never contain a
# dot, and host slugs are reserved handles - so a bare first segment is
# classifiable in the mounts via Tarakan.Hosts.host_segment?/1. The bare
# family is declared first so its literal segments win; when its first
# segment is actually a host, the mount reinterprets the params. That
# only happens for remote repositories literally named "code", "commits" or
# "security" (their security tab is the one known-degraded corner).
#
# These wildcard routes must stay at the bottom of the router so they can
# never shadow literal routes such as /accounts/log-in/:token.
scope "/", TarakanWeb do
pipe_through [:browser]
live_session :repository_browser,
on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
# A bare single segment is a contributor profile (GitHub-style
# /handle). Handles that would shadow a fixed route are reserved at
# registration, so this can never swallow /accounts, /jobs, etc.
live "/:handle", AccountLive.Profile, :show
live "/:owner/:name", RepositoryCodeLive, :entry
live "/:owner/:name/security", RepositoryLive.Show, :show
live "/:owner/:name/commits", RepositoryCommitsLive, :index
live "/:owner/:name/commits/:commit_sha", RepositoryCommitsLive, :show
live "/:owner/:name/code", RepositoryCodeLive, :code_entry
live "/:owner/:name/code/:commit_sha", RepositoryCodeLive, :show
live "/:owner/:name/code/:commit_sha/*path", RepositoryCodeLive, :show
live "/:host/:owner/:name", RepositoryCodeLive, :entry
live "/:host/:owner/:name/security", RepositoryLive.Show, :show
live "/:host/:owner/:name/commits", RepositoryCommitsLive, :index
live "/:host/:owner/:name/commits/:commit_sha", RepositoryCommitsLive, :show
live "/:host/:owner/:name/code", RepositoryCodeLive, :entry
live "/:host/:owner/:name/code/:commit_sha", RepositoryCodeLive, :show
live "/:host/:owner/:name/code/:commit_sha/*path", RepositoryCodeLive, :show
end
end
@doc false
def rate_limit_oauth_ip(conn, _opts) do
key = TarakanWeb.Plugs.ClientIp.remote_ip_bucket(conn)
if TarakanWeb.BrowserRateLimit.allowed?(:oauth_ip, key) do
conn
else
conn
|> put_resp_content_type("text/plain")
|> send_resp(429, "too many requests")
|> halt()
end
end
end