13.0 KiB · text 5af55d9
defmodule GitGudWeb.Router do
use GitGudWeb, :router
import GitGudWeb.UserAuth
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {GitGudWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_scope_for_user
end
pipeline :api do
plug :accepts, ["json", "protobuf"]
end
# No accepts gate — used by the SSH wrapper and post-receive hook,
# which speak plain text.
pipeline :loopback do
end
# Raw pass-through for bytes. Used for the disk-backed storage
# endpoints (LFS objects, artifacts) so Plug.Parsers doesn't try to
# read the body before our streaming reader gets a chance.
pipeline :raw do
end
# JSON endpoints that also need access to the user session (WebAuthn
# ceremony, anywhere the JS calls `fetch()` and we need session data).
# Mirrors :browser minus the html `accepts` gate. CSRF is still
# enforced — the JS attaches the `x-csrf-token` header, which Plug's
# CSRF plug accepts in addition to the form field.
pipeline :browser_api do
plug :accepts, ["json"]
plug :fetch_session
plug :protect_from_forgery
plug :fetch_current_scope_for_user
end
# Forgejo Actions runner protocol (Twirp JSON). Mounted at
# Forgejo runner Twirp endpoints. Newer runners (v10+) hit
# `/api/actions/<package>.<Service>/<Method>` directly via Connect-Go.
# Older runners and `act_runner` hit the same paths under
# `/api/actions/_apis/...`. Both prefixes are mounted to the same
# controller actions for compatibility.
scope "/api/actions", GitGudWeb do
pipe_through :api
# Newer runners (v12+) put Ping in its own `ping.v1` package, and
# the runner-side methods live under `runner.v1`. Older versions
# had Ping under `runner.v1` and the whole tree under `_apis/`.
# All four shapes are mounted to the same controller actions so
# any runner version connects.
post "/ping.v1.PingService/Ping", RunnerController, :ping
post "/runner.v1.PingService/Ping", RunnerController, :ping
post "/runner.v1.RunnerService/Register", RunnerController, :register
post "/runner.v1.RunnerService/Declare", RunnerController, :declare
post "/runner.v1.RunnerService/FetchTask", RunnerController, :fetch_task
post "/runner.v1.RunnerService/FetchSingleTask", RunnerController, :fetch_single_task
post "/runner.v1.RunnerService/UpdateTask", RunnerController, :update_task
post "/runner.v1.RunnerService/UpdateLog", RunnerController, :update_log
# Legacy `_apis/` prefix.
post "/_apis/ping.v1.PingService/Ping", RunnerController, :ping
post "/_apis/runner.v1.PingService/Ping", RunnerController, :ping
post "/_apis/runner.v1.RunnerService/Register", RunnerController, :register
post "/_apis/runner.v1.RunnerService/Declare", RunnerController, :declare
post "/_apis/runner.v1.RunnerService/FetchTask", RunnerController, :fetch_task
post "/_apis/runner.v1.RunnerService/FetchSingleTask", RunnerController, :fetch_single_task
post "/_apis/runner.v1.RunnerService/UpdateTask", RunnerController, :update_task
post "/_apis/runner.v1.RunnerService/UpdateLog", RunnerController, :update_log
end
# Runner-side services for in-flight jobs: artifact upload/download
# + cache. JWT-gated via the per-task token minted by FetchTask.
pipeline :runner_jwt do
plug :accepts, ["json"]
plug GitGudWeb.Plugs.RunnerJwtAuth
end
scope "/api/actions/_apis", GitGudWeb do
pipe_through :runner_jwt
# GitHub Actions cache server v1
get "/artifactcache/cache", ActionsCacheController, :get_cache
post "/artifactcache/caches", ActionsCacheController, :reserve_cache
patch "/artifactcache/caches/:id", ActionsCacheController, :upload_chunk
post "/artifactcache/caches/:id", ActionsCacheController, :finalize
# Pipelines v1 artifact API
scope "/pipelines/workflows/:run_id" do
post "/artifacts", ActionsArtifactController, :create
post "/artifacts/:id/upload", ActionsArtifactController, :upload
patch "/artifacts", ActionsArtifactController, :finalize
get "/artifacts", ActionsArtifactController, :list
get "/artifacts/:id/download", ActionsArtifactController, :download
end
end
# OCI registry token endpoint (zot's `realm`).
scope "/v2", GitGudWeb do
pipe_through :api
get "/token", RegistryController, :token
end
# WebFinger + NodeInfo discovery.
scope "/", GitGudWeb do
pipe_through :api
get "/.well-known/webfinger", WellKnownController, :webfinger
get "/.well-known/nodeinfo", WellKnownController, :nodeinfo_index
get "/nodeinfo/2.1", WellKnownController, :nodeinfo_2_1
end
# Local-only endpoints used by the SSH wrapper and post-receive hook.
# Bind to 127.0.0.1 in production; never expose externally.
scope "/_ssh", GitGudWeb do
pipe_through :loopback
post "/resolve", SshController, :resolve
end
scope "/_hooks", GitGudWeb do
pipe_through :loopback
post "/post-receive", HookController, :post_receive
post "/pre-receive", PreReceiveController, :pre_receive
end
# zot OCI registry → Packages mirror webhook. Shared-secret guarded;
# the controller does its own Bearer-token check.
scope "/_webhooks", GitGudWeb do
pipe_through :api
post "/zot", PackagesWebhookController, :notify
end
# Disk-backed storage. Token-gated; the URL is the capability.
# When the storage impl is S3/Garage, these routes are unused.
scope "/_storage", GitGudWeb do
pipe_through :raw
get "/:bucket/*key", StorageController, :show
put "/:bucket/*key", StorageController, :upload
end
# Authenticated routes FIRST so static `/new` / `/labels` / etc.
# win over the public `:number` / `:slug` bindings that would
# otherwise capture them (`/issues/new` matching `:number => "new"`
# is the canonical footgun).
scope "/", GitGudWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :authenticated_repo,
on_mount: [{GitGudWeb.UserAuth, :require_authenticated}] do
live "/repositories/new", RepoLive.New, :new
live "/r/:owner/:name/issues/new", IssueLive.New, :new
live "/r/:owner/:name/pulls/new", PrLive.New, :new
live "/r/:owner/:name/labels", LabelLive.Index, :index
live "/r/:owner/:name/wiki/_new", WikiLive.Edit, :new
live "/r/:owner/:name/wiki/_edit/:slug", WikiLive.Edit, :edit
live "/r/:owner/:name/settings/webhooks", WebhookLive.Index, :index
live "/r/:owner/:name/settings/deploy-keys", DeployKeyLive.Index, :index
live "/r/:owner/:name/settings/branches", BranchProtectionLive.Index, :index
live "/r/:owner/:name/settings/interaction", InteractionPolicyLive.Edit, :edit
live "/r/:owner/:name/settings/secrets", SecretsLive.Repo, :index
live "/r/:owner/:name/settings/registry-tokens", RegistryTokenLive.Index, :index
live "/r/:owner/:name/settings/runners", RepoLive.SettingsRunners, :index
live "/r/:owner/:name/settings", RepoLive.Settings, :edit
live "/orgs/:handle/settings/profile", OrgLive.SettingsProfile, :index
live "/orgs/:handle/settings/members", OrgLive.Members, :index
live "/orgs/:handle/settings/secrets", SecretsLive.Org, :index
live "/orgs/:handle/settings/runners", OrgLive.SettingsRunners, :index
live "/orgs/:handle/settings/webhooks", WebhookLive.Org, :index
live "/orgs/:handle/settings/labels", OrgLive.Labels, :index
live "/users/ci", UserLive.Ci, :index
live "/r/:owner/:name/fork", RepoLive.Fork, :new
live "/orgs", OrgLive.Index, :index
end
live_session :admin,
on_mount: [{GitGudWeb.UserAuth, :require_admin}] do
live "/admin/federation", AdminFederationLive.Index, :index
end
end
scope "/", GitGudWeb do
pipe_through :browser
get "/", PageController, :home
live_session :public_repo,
on_mount: [{GitGudWeb.UserAuth, :mount_current_scope}] do
# Public read-only browsing. Visibility is enforced inside the
# LiveView itself.
live "/repositories", RepoLive.Index, :index
live "/u/:handle", UserLive.Profile, :show
live "/orgs/:handle", OrgLive.Show, :show
live "/orgs/:handle/actions", OrgLive.Actions, :index
live "/r/:owner/:name", RepoLive.Show, :show
live "/r/:owner/:name/tree/:ref/*path", RepoLive.Tree, :show
live "/r/:owner/:name/blob/:ref/*path", RepoLive.Blob, :show
live "/r/:owner/:name/commits/:ref", RepoLive.Log, :index
live "/r/:owner/:name/commit/:sha", RepoLive.Commit, :show
live "/r/:owner/:name/issues", IssueLive.Index, :index
live "/r/:owner/:name/issues/:number", IssueLive.Show, :show
live "/r/:owner/:name/pulls", PrLive.Index, :index
live "/r/:owner/:name/pulls/:number", PrLive.Show, :show
live "/r/:owner/:name/wiki", WikiLive.Index, :index
live "/r/:owner/:name/wiki/:slug", WikiLive.Show, :show
live "/r/:owner/:name/actions", WorkflowLive.Index, :index
live "/r/:owner/:name/actions/:number", WorkflowLive.Show, :show
get "/r/:owner/:name/actions/:number/artifacts/:id",
WorkflowArtifactController,
:download
live "/r/:owner/:name/forks", RepoLive.Forks, :index
live "/r/:owner/:name/compare", RepoLive.Compare, :index
live "/r/:owner/:name/packages", RepoLive.Packages, :index
live "/r/:owner/:name/stats/languages", RepoLive.Languages, :index
live "/r/:owner/:name/stats/committers", RepoLive.Committers, :index
live "/actors/:id", ActorLive.Show, :show
end
end
# Gitea/GitHub-compatible REST API. forgejo-runner advertises
# GITHUB_API_URL=<forge>/api/v1, so actions (docker/metadata-action, …)
# hit these. Only the slice runner-driven actions need is implemented;
# auth is handled per-action in the controller (public read / RunnerJwt).
scope "/api/v1", GitGudWeb.Api.V1 do
pipe_through :api
get "/repos/:owner/:name", RepositoryController, :show
get "/repos/:owner/:name/commits/:sha", RepositoryController, :commit
# Write endpoints — Personal Access Token auth (handled in-controller).
post "/orgs/:org/repos", OrgController, :create_repo
delete "/orgs/:org/repos/:name", OrgController, :delete_repo
end
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:git_gud, :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: GitGudWeb.Telemetry
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
## Authentication routes
scope "/", GitGudWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user,
on_mount: [{GitGudWeb.UserAuth, :require_authenticated}] do
live "/users/settings", UserLive.Settings, :edit
live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
live "/users/settings/ssh-keys", SshKeyLive.Index, :index
live "/users/settings/tokens", PersonalAccessTokenLive.Index, :index
live "/users/settings/invites", UserLive.Invites, :index
live "/users/settings/two-factor", UserLive.TwoFactorSetup, :index
end
post "/users/update-password", UserSessionController, :update_password
end
# WebAuthn ceremony — JSON endpoints that need session access.
# Authed enrollment goes through `require_authenticated_user`; the
# auth (mid-2FA) endpoints don't, since the user isn't logged in yet.
scope "/api/webauthn", GitGudWeb do
pipe_through [:browser_api, :require_authenticated_user]
post "/register/start", WebauthnController, :register_start
post "/register/finish", WebauthnController, :register_finish
end
scope "/api/webauthn", GitGudWeb do
pipe_through :browser_api
post "/auth/start", WebauthnController, :auth_start
post "/auth/finish", WebauthnController, :auth_finish
end
scope "/", GitGudWeb do
pipe_through [:browser]
live_session :current_user,
on_mount: [{GitGudWeb.UserAuth, :mount_current_scope}] do
live "/users/register", UserLive.Registration, :new
live "/users/log-in", UserLive.Login, :new
live "/users/log-in/two-factor", UserLive.TwoFactorChallenge.Live, :new
live "/users/log-in/:token", UserLive.Confirmation, :new
end
post "/users/log-in", UserSessionController, :create
delete "/users/log-out", UserSessionController, :delete
# 2FA completion — the user isn't logged in yet at this point;
# their pending state lives in the session under
# `:two_factor_user_id`. The controller validates that against the
# `u` query param and only logs in if they match.
get "/users/log-in/two-factor/complete",
UserLive.TwoFactorChallenge,
:complete
end
end