package main // 🤖 donate.go — checkout for the /donate page on the marketing site. Eleven // has no funding; the page says so and asks nothing — but if someone wants // to back the ideas (defiance of big tech, of over-complexity in software // distribution; proceeds into the 11 factor principles and affordable, // top-tier software), this mints the Stripe Checkout Session that takes // their money. Same deliberate shape as the Enterprise page's checkout // (enterprise.go): its own Stripe key (LCHAT_STRIPE_DONATE_KEY, sandbox // until the explicit live flip), CORS pinned to the marketing origin, fixed // server-side redirects, a global mint rate limit, and amounts constrained // server-side. Pay what you want, floor €1.10 — the elevens' smallest coin. import ( "encoding/json" "fmt" "io" "log" "net/http" "net/url" "strconv" "strings" "time" ) const ( // Minor units (cents/pence — identical numerals in every currency, like // the Enterprise price list; we don't do exchange rates). donateMinMinor = 110 // €1.10 — below this Stripe's fixed fee eats the gift donateMaxMinor = 99_999_999 // Stripe's 8-digit single-payment cap ) var donateCurrencies = map[string]bool{"eur": true, "usd": true, "gbp": true} // donateCheckout holds the page's Stripe config. nil on cloudServer = the // feature is off and the endpoint answers 501. type donateCheckout struct { key string apiBase string origin string client *http.Client limiter mintLimiter } func newDonateCheckout(key, apiBase, origin string) *donateCheckout { key = strings.TrimSpace(key) if key == "" { return nil } return &donateCheckout{ key: key, apiBase: apiBase, origin: strings.TrimRight(origin, "/"), client: &http.Client{Timeout: 20 * time.Second}, limiter: mintLimiter{max: 30, window: time.Minute}, } } // donateCORS stamps the CORS headers; single marketing origin, never *. func (s *cloudServer) donateCORS(w http.ResponseWriter) bool { if s.donate == nil { return false } h := w.Header() h.Set("Access-Control-Allow-Origin", s.donate.origin) h.Set("Vary", "Origin") return true } // donatePreflightHandler: OPTIONS /api/donate/checkout. func (s *cloudServer) donatePreflightHandler(w http.ResponseWriter, r *http.Request) { if !s.donateCORS(w) { w.WriteHeader(http.StatusNotImplemented) return } h := w.Header() h.Set("Access-Control-Allow-Methods", "POST") h.Set("Access-Control-Allow-Headers", "Content-Type") h.Set("Access-Control-Max-Age", "86400") w.WriteHeader(http.StatusNoContent) } // donateCheckoutHandler: POST /api/donate/checkout — mint a one-time // Checkout Session. Body: {"amount_minor": n, "currency": "eur"}. func (s *cloudServer) donateCheckoutHandler(w http.ResponseWriter, r *http.Request) { if !s.donateCORS(w) { writeControlErr(w, http.StatusNotImplemented, "unavailable", "donations are not configured") return } d := s.donate if !d.limiter.allow(time.Now()) { writeControlErr(w, http.StatusTooManyRequests, "slow_down", "the till is busy — try again in a minute") return } var req struct { AmountMinor int `json:"amount_minor"` Currency string `json:"currency"` } if err := json.NewDecoder(io.LimitReader(r.Body, 4<<10)).Decode(&req); err != nil { writeControlErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body") return } cur := strings.ToLower(strings.TrimSpace(req.Currency)) if !donateCurrencies[cur] { writeControlErr(w, http.StatusBadRequest, "bad_request", "currency must be eur, usd or gbp") return } if req.AmountMinor < donateMinMinor { writeControlErr(w, http.StatusBadRequest, "bad_request", "the smallest donation is 1.10 — below that the card fee eats the gift") return } if req.AmountMinor > donateMaxMinor { writeControlErr(w, http.StatusBadRequest, "over_stripe_cap", "Stripe cannot carry this much money in one payment — split it, or talk to us") return } checkoutURL, err := d.createSession(cur, req.AmountMinor) if err != nil { log.Printf("cloud: donate checkout (%s %d): %v", cur, req.AmountMinor, err) writeControlErr(w, http.StatusBadGateway, "checkout_failed", "couldn't start the checkout — please try again") return } writeJSON(w, http.StatusOK, map[string]any{"url": checkoutURL}) } // createSession builds the hosted page: one inline price_data line item, no // customer identity, fixed redirects back to /donate/. func (d *donateCheckout) createSession(currency string, amountMinor int) (string, error) { form := url.Values{} form.Set("mode", "payment") form.Set("line_items[0][price_data][currency]", currency) form.Set("line_items[0][price_data][product_data][name]", "Donation to Eleven") form.Set("line_items[0][price_data][unit_amount]", strconv.Itoa(amountMinor)) form.Set("line_items[0][quantity]", "1") form.Set("metadata[donation]", "1") form.Set("success_url", d.origin+"/donate/?thanks=1") form.Set("cancel_url", d.origin+"/donate/") req, err := http.NewRequest("POST", d.apiBase+"/v1/checkout/sessions", strings.NewReader(form.Encode())) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+d.key) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := d.client.Do(req) if err != nil { return "", err } defer resp.Body.Close() var body struct { URL string `json:"url"` Error struct { Message string `json:"message"` } `json:"error"` } if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&body); err != nil { return "", fmt.Errorf("stripe: HTTP %d (unparseable body)", resp.StatusCode) } if resp.StatusCode != http.StatusOK || body.URL == "" { return "", fmt.Errorf("stripe: HTTP %d: %s", resp.StatusCode, body.Error.Message) } return body.URL, nil }