| 1 | Rails.application.routes.draw do |
| 2 | root "pages#home" |
| 3 | |
| 4 | # Static pages — declared before the "/:username" matcher so these literal |
| 5 | # slugs aren't read as profiles. |
| 6 | get "/code", to: "pages#code", as: :code |
| 7 | get "/about", to: "pages#about", as: :about |
| 8 | get "/contact", to: "pages#contact", as: :contact |
| 9 | get "/privacy", to: "pages#privacy", as: :privacy |
| 10 | get "/terms", to: "pages#terms", as: :terms |
| 11 | |
| 12 | # Public changelog: an index and one page per release. The version constraint |
| 13 | # (v<major>.<minor>.<patch>) keeps "/changelog/anything-else" from matching, |
| 14 | # and both are declared before the "/:username" matcher so the literal |
| 15 | # "changelog" slug isn't read as a profile. |
| 16 | get "/changelog", to: "changelog#index", as: :changelog |
| 17 | get "/changelog/:version", to: "changelog#show", as: :changelog_version, |
| 18 | constraints: { version: /v\d+\.\d+\.\d+/ } |
| 19 | |
| 20 | # XML sitemap for search engines (referenced from public/robots.txt). |
| 21 | get "/sitemap.xml", to: "sitemaps#index", defaults: { format: "xml" }, as: :sitemap |
| 22 | |
| 23 | # Open Graph share-card images (1200×630 PNG). Rendered server-side and cached |
| 24 | # so social/crawler unfurls get a real preview card. Declared before the |
| 25 | # "/:username" matcher; the dotted-name constraint mirrors the repo routes so |
| 26 | # names like "Qwen2.5-GGUF" survive. `format: false` keeps the literal ".png". |
| 27 | get "/og.png", to: "og_images#default", as: :og_default_image, format: false |
| 28 | get "/og/:username/:repository.png", to: "og_images#show", as: :og_image, |
| 29 | constraints: { repository: /[^\/.][^\/]*/ }, format: false |
| 30 | |
| 31 | # Dev panel — only mounted in development |
| 32 | if Rails.env.development? |
| 33 | namespace :dev do |
| 34 | get "environment", to: "panel#show" |
| 35 | put "environment", to: "panel#update" |
| 36 | end |
| 37 | end |
| 38 | |
| 39 | # Auth — email/password form |
| 40 | get "/auth", to: "sessions#new", as: :signin |
| 41 | post "/auth", to: "sessions#create", as: :auth_create |
| 42 | delete "/auth/signout", to: "sessions#destroy", as: :signout |
| 43 | |
| 44 | # Auth — "Continue with GitHub" / "Continue with Google" (brokered by smbCloud Auth) |
| 45 | get "/auth/github", to: "oauth/github#start", as: :github_auth |
| 46 | get "/auth/github/callback", to: "oauth/github#callback", as: :github_auth_callback |
| 47 | get "/auth/google", to: "oauth/google#start", as: :google_auth |
| 48 | get "/auth/google/callback", to: "oauth/google#callback", as: :google_auth_callback |
| 49 | |
| 50 | # Signup |
| 51 | get "/auth/signup", to: "registrations#new", as: :signup |
| 52 | post "/auth/signup", to: "registrations#create", as: :signup_create |
| 53 | |
| 54 | # Email confirmation landing — smbCloud confirms the token server-side, then |
| 55 | # redirects here with ?status=success or ?status=failed&message=... |
| 56 | get "/auth/confirmed", to: "confirmations#confirmed", as: :auth_confirmed |
| 57 | |
| 58 | # Resend confirmation email |
| 59 | get "/auth/confirmation/resend", to: "confirmations#new", as: :resend_confirmation |
| 60 | post "/auth/confirmation/resend", to: "confirmations#create" |
| 61 | |
| 62 | # Reset password — request reset instructions by email |
| 63 | get "/auth/password/reset", to: "passwords#new", as: :reset_password |
| 64 | post "/auth/password/reset", to: "passwords#create" |
| 65 | |
| 66 | # Reset password — set a new password from the email link |
| 67 | # (smbCloud redirects here with ?reset_password_token=...) |
| 68 | get "/auth/password/edit", to: "passwords#edit", as: :edit_password |
| 69 | post "/auth/password/edit", to: "passwords#update" |
| 70 | |
| 71 | # Discovery surfaces — declared before the "/:username" matcher so they |
| 72 | # aren't read as profiles. |
| 73 | get "/models", to: "models#index", as: :models # open-weights model library |
| 74 | get "/repos", to: "repositories#explore", as: :repos # public code repositories |
| 75 | |
| 76 | # Repository creation |
| 77 | get "/new", to: "repositories#new", as: :new_repository |
| 78 | post "/new", to: "repositories#create" |
| 79 | |
| 80 | # Import from GitHub (mirror + migrate). Declared before the "/:username" |
| 81 | # matcher so "/import" isn't read as a profile. |
| 82 | # |
| 83 | # The GitHub OAuth connect routes are declared before the resources block so |
| 84 | # "/import/github/*" isn't swallowed by the "/import/:id" show route. This is |
| 85 | # our own GitHub OAuth app (separate from smbCloud sign-in); the token it mints |
| 86 | # can read/clone the user's GitHub repos. |
| 87 | get "/import/github/connect", to: "github_connections#connect", as: :github_import_connect |
| 88 | get "/import/github/callback", to: "github_connections#callback", as: :github_import_callback |
| 89 | delete "/import/github/disconnect", to: "github_connections#disconnect", as: :github_import_disconnect |
| 90 | |
| 91 | resources :imports, path: "import", only: %i[index new create show], |
| 92 | constraints: { id: /\d+/ } do |
| 93 | post :retry, on: :member |
| 94 | end |
| 95 | |
| 96 | # GitHub push webhook → immediate mirror sync (HMAC-authenticated). |
| 97 | # Distinct from the GitHub App PR-review webhook at POST /github/webhooks. |
| 98 | post "/webhooks/github", to: "mirror_webhooks#create" |
| 99 | |
| 100 | # Settings |
| 101 | get "/settings", to: "users#settings", as: :settings |
| 102 | patch "/settings", to: "users#update_settings" |
| 103 | |
| 104 | # Admin console — internal ops/business dashboard. Gated by `require_admin!`. |
| 105 | # Declared before the "/:username" matcher so "/admin" isn't read as a profile. |
| 106 | namespace :admin do |
| 107 | root "dashboard#show" |
| 108 | get "dashboard", to: "dashboard#show" |
| 109 | |
| 110 | resources :users, only: %i[index show] do |
| 111 | member do |
| 112 | patch :grant_admin |
| 113 | patch :revoke_admin |
| 114 | end |
| 115 | end |
| 116 | |
| 117 | resources :repositories, only: %i[index show] |
| 118 | resources :subscriptions, only: %i[index] |
| 119 | end |
| 120 | |
| 121 | # Billing — siGit Code plans (web). Checkout/portal happen on the web. |
| 122 | get "/billing", to: "billing#show", as: :billing |
| 123 | post "/billing/checkout", to: "billing#checkout", as: :billing_checkout |
| 124 | post "/billing/portal", to: "billing#portal", as: :billing_portal |
| 125 | |
| 126 | # JSON API — token-based auth for the siGit Code & Deploy desktop app. |
| 127 | # Declared before the catch-all "/:username" route so "/api/..." isn't |
| 128 | # swallowed by the username matcher. |
| 129 | namespace :api do |
| 130 | namespace :v1 do |
| 131 | post "auth/sign_in", to: "sessions#create" |
| 132 | delete "auth/sign_out", to: "sessions#destroy" |
| 133 | post "auth/sign_up", to: "registrations#create" |
| 134 | post "auth/confirmation/resend", to: "confirmations#create" |
| 135 | post "auth/password/reset", to: "passwords#create" |
| 136 | # "Continue with GitHub" / "Continue with Google" for browser SPA clients |
| 137 | # (code.sigit.si). A server-side redirect that brokers the smbCloud flow |
| 138 | # so the app secret never reaches the client; smbCloud bounces back to the |
| 139 | # SPA's redirect_uri with an access_token (the same bearer the API expects). |
| 140 | get "auth/github", to: "oauth/github#start" |
| 141 | get "auth/google", to: "oauth/google#start" |
| 142 | get "me", to: "me#show" |
| 143 | delete "me", to: "me#destroy" |
| 144 | get "repos", to: "repos#index" |
| 145 | post "repos", to: "repos#create" |
| 146 | post "git_credentials", to: "git_credentials#create" |
| 147 | post "chat/completions", to: "chat_completions#create" |
| 148 | get "billing", to: "billing#show" |
| 149 | post "billing/checkout", to: "billing#checkout" |
| 150 | post "billing/portal", to: "billing#portal" |
| 151 | |
| 152 | # Model Context Protocol endpoint (Streamable HTTP, stateless JSON-RPC). |
| 153 | post "mcp", to: "mcp#handle" # JSON-RPC messages from the client |
| 154 | get "mcp", to: "mcp#stream" # optional server->client SSE (returns 405 here) |
| 155 | |
| 156 | # siGit Code Cloud Sessions — persisted, resumable conversations. |
| 157 | resources :cloud_sessions, path: "sessions", |
| 158 | only: %i[index create show update destroy] do |
| 159 | post "messages", on: :member, to: "cloud_sessions#create_message" |
| 160 | end |
| 161 | end |
| 162 | end |
| 163 | |
| 164 | # Stripe webhooks for siGit Code billing (authenticated by Stripe signature). |
| 165 | post "/stripe/webhooks", to: "stripe_webhooks#create" |
| 166 | |
| 167 | # GitHub App webhooks for siGit Code PR reviews (authenticated by HMAC signature). |
| 168 | post "/github/webhooks", to: "github_webhooks#create" |
| 169 | |
| 170 | # Git Smart HTTP — clone/fetch over token-authenticated HTTPS. Declared before |
| 171 | # the catch-all "/:username" routes; the `*.git` constraint keeps them from |
| 172 | # matching normal repo-browsing URLs. |
| 173 | get "/:user/:repo/info/refs", to: "git_http#info_refs", |
| 174 | constraints: { repo: /[^\/]+\.git/ } |
| 175 | post "/:user/:repo/git-upload-pack", to: "git_http#upload_pack", |
| 176 | constraints: { repo: /[^\/]+\.git/ } |
| 177 | post "/:user/:repo/git-receive-pack", to: "git_http#receive_pack", |
| 178 | constraints: { repo: /[^\/]+\.git/ } |
| 179 | |
| 180 | # User profile (must come before repository routes) |
| 181 | get "/:username", to: "users#show", as: :user_profile, |
| 182 | constraints: { username: /[a-z0-9][a-z0-9\-]{0,38}/ } |
| 183 | |
| 184 | # Repository routes (in order of specificity) |
| 185 | get "/:username/:repository/commits/:branch", to: "commits#index", as: :repository_commits, |
| 186 | constraints: { branch: /[^\/]+/, repository: /[^\/.][^\/]*/ } |
| 187 | get "/:username/:repository/ci-cd", to: "repositories#cicd", as: :repository_cicd |
| 188 | get "/:username/:repository/commit/:sha", to: "commits#show", as: :repository_commit |
| 189 | |
| 190 | # Pull requests — creation-only backend already existed (PullRequestService, |
| 191 | # exposed over MCP); these add the web surface on top of it. |
| 192 | get "/:username/:repository/pulls", to: "pull_requests#index", as: :repository_pull_requests |
| 193 | get "/:username/:repository/pulls/new", to: "pull_requests#new", as: :new_repository_pull_request |
| 194 | post "/:username/:repository/pulls", to: "pull_requests#create" |
| 195 | get "/:username/:repository/pull/:number", to: "pull_requests#show", as: :repository_pull_request, |
| 196 | constraints: { number: /\d+/ } |
| 197 | post "/:username/:repository/pull/:number/comments", to: "pull_requests#add_comment", |
| 198 | as: :repository_pull_request_comments, constraints: { number: /\d+/ } |
| 199 | post "/:username/:repository/pull/:number/close", to: "pull_requests#close", as: :close_repository_pull_request, |
| 200 | constraints: { number: /\d+/ } |
| 201 | post "/:username/:repository/pull/:number/reopen", to: "pull_requests#reopen", as: :reopen_repository_pull_request, |
| 202 | constraints: { number: /\d+/ } |
| 203 | post "/:username/:repository/pull/:number/merge", to: "pull_requests#merge", as: :merge_repository_pull_request, |
| 204 | constraints: { number: /\d+/ } |
| 205 | get "/:username/:repository/blob/:branch/*path", to: "blobs#show", as: :repository_blob, format: false |
| 206 | get "/:username/:repository/raw/:branch/*path", to: "blobs#raw", as: :repository_blob_raw, format: false |
| 207 | get "/:username/:repository/tree/:branch/*path", to: "repositories#tree", as: :repository_tree |
| 208 | get "/:username/:repository/tree/:branch", to: "repositories#tree", as: :repository_branch |
| 209 | post "/:username/:repository/star", to: "stars#create", as: :repository_star, |
| 210 | constraints: { repository: /[^\/.][^\/]*/ }, format: false |
| 211 | post "/:username/:repository/mirror/detach", to: "mirrors#detach", as: :repository_mirror_detach, |
| 212 | constraints: { repository: /[^\/.][^\/]*/ }, format: false |
| 213 | post "/:username/:repository/mirror/sync", to: "mirrors#sync", as: :repository_mirror_sync, |
| 214 | constraints: { repository: /[^\/.][^\/]*/ }, format: false |
| 215 | # `format: false` keeps dotted names (e.g. "Qwen2.5-3B-Instruct-GGUF") intact |
| 216 | # rather than treating the dot as a response-format separator. |
| 217 | get "/:username/:repository", to: "repositories#show", as: :repository, |
| 218 | constraints: { repository: /[^\/.][^\/]*/ }, format: false |
| 219 | |
| 220 | # Health check |
| 221 | get "up" => "rails/health#show", as: :rails_health_check |
| 222 | end |