package console import ( "context" "crypto/subtle" "fmt" "log" "net/http" "strconv" "strings" "time" "github.com/carlosframework/platform/internal/appconfig" ) // handleAppSwap is the Deploys card's form (spec §4.1, §9.2): any member of // the app, gated in AGENTS.md's order (session, CSRF, then the byte-identical // 404 for an app that is not the caller's), as the restart action is. func (s *Server) handleAppSwap(w http.ResponseWriter, r *http.Request) { addr, raw, acct, _, ok := s.account(w, r) if !ok { return } if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 { s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.") return } app := r.PathValue("app") has, err := s.store.HasAppIn(acct.ID, app) if err != nil { s.renderMessage(w, r, http.StatusInternalServerError, "Error", "Could not read your apps. Please try again.") return } if !has { http.NotFound(w, r) return } back := func(kind, msg string) { s.setFlash(w, kind, msg) http.Redirect(w, r, appSettingsURL(acct.Sqid, app, "general"), http.StatusSeeOther) } if s.cfg.Releases == nil { back("err", swapCopy.NoBucket) return } swap := strings.TrimSpace(r.FormValue("swap")) n, err := strconv.Atoi(strings.TrimSpace(r.FormValue("swap_concurrency"))) if (swap != appconfig.SwapOverlap && swap != appconfig.SwapHold) || err != nil || n < 1 { back("err", swapCopy.Invalid) return } if !s.allowWrite(Principal{Kind: PrincipalUser, Address: addr}, time.Now()) { back("err", swapCopy.RateLimited) return } ctx, cancel := context.WithTimeout(r.Context(), configStoreTimeout) defer cancel() got, err := appconfig.SetSwapPolicy(ctx, s.cfg.Releases, acct.Sqid, app, swap, n) if err != nil { log.Printf("console: swap policy %s/%s: %v", acct.Sqid, app, err) back("err", swapCopy.StoreFailed) return } s.audit(acct, app, addr, "swap-policy", fmt.Sprintf(apiSwapAudit, app, got.Swap, got.Concurrency, "console")) s.bumpSerial(ctx) back("ok", fmt.Sprintf(swapCopy.Saved, got.Swap, got.Concurrency)) } // loadSwapPolicy fills the Deploys card. A failed read says so rather than // showing the defaults as if they were the setting (ErrLevelUnread's rule). func (s *Server) loadSwapPolicy(ctx context.Context, p *page, acct *Account) { if s.cfg.Releases == nil { p.SwapUnread = true return } sp, err := appconfig.GetSwapPolicy(ctx, s.cfg.Releases, acct.Sqid, p.AppName) if err != nil { log.Printf("console: swap policy %s/%s: %v", acct.Sqid, p.AppName, err) p.SwapUnread = true return } p.SwapPolicy = sp }