From b92132474b4a357a6cb60c8c8bbc76e1e0b45b1b Mon Sep 17 00:00:00 2001 From: marcusDenslow Date: Tue, 14 Apr 2026 15:26:36 +0200 Subject: [PATCH] various error checks and formatting --- NEXT.md | 160 ++++++++++++++++++++++++++------- api/handlers/addresses.go | 20 +++-- api/handlers/addresses_test.go | 1 - api/handlers/auth.go | 10 ++- api/handlers/cards.go | 24 +++-- api/handlers/cart.go | 43 ++++++--- api/handlers/products.go | 1 - api/handlers/ssh_keys.go | 14 +-- api/handlers/webhook.go | 11 ++- api/routes/routes.go | 2 +- pkg/bring/bring.go | 1 - pkg/config/config.go | 28 +++--- pkg/models/cart.go | 2 +- pkg/tui/menu.go | 13 ++- pkg/tui/qrfefe/qr.go | 2 +- pkg/tui/shop.go | 4 +- 16 files changed, 242 insertions(+), 94 deletions(-) diff --git a/NEXT.md b/NEXT.md index a14c6e2..f8b6135 100644 --- a/NEXT.md +++ b/NEXT.md @@ -1,47 +1,145 @@ # Next Session -## Feature: SSH Key Management in Account Page +## Where the Project is Genuinely Stronger than the Reference -Following the same pattern as address/card management we just built, add an -"ssh keys" section to the account page so users can see and delete their saved -SSH public keys. +Before the gap list — these are intentional design wins worth keeping: -### What needs doing +- **Server-side authoritative cart** — reference uses client-side optimistic updates which can + diverge across sessions. Project's DB-backed cart can't desync. +- **Idempotent payments** — order record is created *before* the Stripe charge. If the TUI + crashes mid-flow, there's a reconciliation trail. Reference has no explicit idempotency. +- **Atomic checkout transaction** — order + cart clear + address/card sync happen in one + transaction. Reference handles this via webhook reconciliation (weaker). +- **Multi-key SSH support** — reference only trusts the single SSH transport key, can't manage + per-user keys. Project's DB model supports revocation and multiple keys per user. +- **Rate limiting** — auth (20/min), card creation (15/min), checkout (10/min). Reference has none visible. -1. **Backend** — check if `api/handlers/` has SSH key endpoints (list, delete). - If not, add them following the same pattern as address/card handlers. - Model is likely already in `pkg/models/` or needs adding. +--- -2. **`pkg/models/coffee.go`** — add `"ssh keys"` to `AccountMenuItems` +## Critical Fixes -3. **`pkg/tui/model.go`** — add state fields: - ```go - SSHKeys []models.SSHKey - SSHKeysLoaded bool - SSHKeyListFocused bool - AccountSSHKeyCursor int - AccountSSHKeyDeleting *int - ``` - Add `fetchSSHKeysCmd()` and `deleteSSHKeyCmd(id)` following the same - pattern as `fetchCardsCmd` / `deleteCardCmd`. +### 1. SSH public key stored in TUI memory +**File:** `main.go:59` (`SSHPublicKeyStr` passed into model) -4. **`pkg/tui/account.go`** — add `case "ssh keys":` in `BuildAccountView` - showing each key's fingerprint/comment, and handle j/k, x, y/n, esc in - `AccountUpdate` — copy the addresses block exactly. +The reference stores only the fingerprint in TUI context — never the full key material. Storing +the full public key in the TUI model means it lives in memory for the session duration. If the +process is inspected or crashes with a heap dump, key material is exposed. -5. **`pkg/tui/update.go`** — fetch SSH keys when switching to account page - (same as orders). +Fix: pass fingerprint only into the TUI. The full key is only needed during auth, not in the UI. -### After that — API Tokens -Same pattern again but for API access tokens. Reference implementation is in -`terminal-shop-source/packages/go/pkg/tui/tokens.go`. Tokens have a "create" -action in addition to delete (generates a new token and shows it once). +### 2. Silent database failures (8 locations) +Handlers return 200 OK even when the DB operation failed. Full list: +- `api/handlers/ssh_keys.go:53` — `db.Delete()` result ignored +- `api/handlers/addresses.go:174, 200, 203` — `db.Delete()`, `db.Update()`, `db.Save()` ignored +- `api/handlers/cards.go:122, 125` — `db.Update()` results ignored +- `api/handlers/cart.go:120, 130, 132, 167, 200, 217` — `db.Delete()`, `db.Create()`, `db.Update()` ignored + +Pattern to use everywhere: +```go +if err := db.Delete(&key).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error()) + return +} +``` + +### 3. ErrorMsg never cleared +**File:** `pkg/tui/model.go` (ErrorMsg string field) + +Once set, `ErrorMsg` persists until the next state change. If a fetch fails then the user +navigates away and back, the stale error is still showing. The reference uses an explicit +`*VisibleError` type with a dismiss action. + +Fix: clear `ErrorMsg` when entering a new page, or add explicit dismiss logic. + +### 4. No pagination on list endpoints +`GET /api/v1/addresses`, `/cards`, `/orders` return all records. Fine now but a structural +issue for production. The reference's SDK handles pagination. Add `limit`/`offset` query +params and cap at a sensible default (e.g. 50). + +### 5. Cart quantity=0 rows accumulate +When items are removed from cart, rows stay in the DB with quantity=0 (soft delete). Add a +cleanup job or switch to hard deletes — quantity=0 has no semantic value. + +--- + +## TUI Architecture: Flat State vs Nested State + +The reference uses nested state substruct per feature: +```go +type model struct { + cartState cartState + accountState accountState + paymentState paymentState +} +``` + +The project uses a flat struct with ~40 boolean flags and nullable pointers. This works but +as the feature set grows it becomes hard to reason about — e.g. what happens when +`ShowingMenu=true` AND `CheckoutInProgress=true`? The flat model doesn't enforce invariants. + +This isn't a quick fix — it's a refactor to consider before adding more features. Each new +page/feature added to the flat model increases the mental load of reading `Update()`. + +--- + +## Missing Features (Reference Has, Project Doesn't) + +### API Token Management +The reference has a full tokens page: list tokens, create new (show secret once), revoke. +This is the standard way to give programmatic access without exposing the user's session JWT. + +Required pieces: +1. `pkg/models/token.go` — Token model (ID, UserID, Secret hash, CreatedAt, LastUsedAt, Comment) +2. `api/handlers/tokens.go` — GetTokens, CreateToken (return plaintext once), DeleteToken +3. Routes in `api/routes/routes.go` +4. API client methods in `pkg/api/client.go` +5. TUI account section — list + create + revoke. Create shows secret in a "copy this now" panel, + any keypress clears it permanently. + +Reference: `terminal-shop-source/packages/go/pkg/tui/tokens.go` + +### User Profile Editing +No `PUT /api/v1/profile` endpoint exists. Users can't update name or email after registration. + +### Product Variants +Without variants the shop can only sell one SKU per product. This is the most limiting gap for +a real store. The reference models variant as a separate entity with its own price and inventory. + +Requires: Variant model, DB migration, API response change, TUI variant selector in shop detail +panel, cart items reference VariantID not CoffeeID. + +### Auto-registration on First SSH Connection +The reference registers users silently on first connection — zero friction. The project requires +an explicit registration step. This is a UX gap, not a security one (both are equally sound +auth-wise). Worth closing if public-facing. + +--- + +## Token Refresh Recovery +If the background token refresh fails 5 times, the user is stuck with an expired token but +there's no recovery path — no prompt, no re-auth. Add a fallback: after N failures, show +an error with instructions to reconnect. + +--- + +## Polish (Lower Priority) + +- **Inconsistent error code string:** `api/handlers/addresses.go:169` uses `"ADDRESS NOT FOUND"` + (space) instead of `"ADDRESS_NOT_FOUND"` (underscore) like the rest of the codebase. +- **Splash screen animation:** Reference has a blinking cursor on the logo (700ms tick). + `pkg/tui/splash.go` exists but is static. +- **Per-field card validation:** Reference validates each form field inline. Project validates + on submit only. Minor UX improvement. +- **Theme system:** 40+ hardcoded hex colors in TUI files. `pkg/tui/theme/` directory exists + but colors are duplicated everywhere. Low priority until the UI stabilises. + +--- + +## Staging Environment (Deferred) -### Staging environment (deferred) -Full setup guide is in the conversation history. Short version: - Create `staging` git branch - `docker-compose.staging.yml` already written and committed -- On server: clone repo into `~/terminalShop-staging`, create `.env.staging`, +- On server: clone into `~/terminalShop-staging`, create `.env.staging`, run `docker compose -f docker-compose.staging.yml --project-name staging up -d` - Add `staging-api.sshops.uk` to Caddyfile - Add `host.docker.internal:host-gateway` to Caddy in `docker-compose.prod.yml` diff --git a/api/handlers/addresses.go b/api/handlers/addresses.go index 5d42ee4..d34da77 100644 --- a/api/handlers/addresses.go +++ b/api/handlers/addresses.go @@ -76,7 +76,7 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) { switch req.Country { case "US": if h.shippoKey == "" { - utils.RespondError(w, http.StatusBadRequest, "CONFIG_ERROR", "shippo not configured for US address validation", nil) + utils.RespondError(w, http.StatusInternalServerError, "CONFIG_ERROR", "shippo not configured for US address validation", nil) return } client := shippo.NewClient(h.shippoKey) @@ -166,12 +166,14 @@ func (h *AddressHandler) DeleteAddress(w http.ResponseWriter, r *http.Request) { var address models.Address if err := db.Where("id = ? AND user_id = ?", id, userID).First(&address).Error; err != nil { - utils.RespondError(w, http.StatusNotFound, "ADDRESS NOT FOUND", "address not found", nil) + utils.RespondError(w, http.StatusNotFound, "ADDRESS_NOT_FOUND", "address not found", nil) return } - // ownership is guaranteed by the WHERE clause above - db.Delete(&address) + if err := db.Delete(&address).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete address", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "message": "address successfully deleted", @@ -197,10 +199,16 @@ func (h *AddressHandler) SetDefaultAddress(w http.ResponseWriter, r *http.Reques return } - db.Model(&models.Address{}).Where("user_id = ?", userID).Update("is_default", false) + if err := db.Model(&models.Address{}).Where("user_id = ?", userID).Update("is_default", false).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to update address", nil) + return + } address.IsDefault = true - db.Save(&address) + if err := db.Save(&address).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to save address", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "address": address, diff --git a/api/handlers/addresses_test.go b/api/handlers/addresses_test.go index 18e7999..3094e01 100644 --- a/api/handlers/addresses_test.go +++ b/api/handlers/addresses_test.go @@ -158,4 +158,3 @@ func TestGetAddressesWithData(t *testing.T) { t.Errorf("expected 2 addresses, got: %d", len(resp.Data.Addresses)) } } - diff --git a/api/handlers/auth.go b/api/handlers/auth.go index db16df1..db053f3 100644 --- a/api/handlers/auth.go +++ b/api/handlers/auth.go @@ -79,7 +79,7 @@ func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) { return } } else if err != nil { - utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "database error", nil) + utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to look up user", nil) return } @@ -89,7 +89,13 @@ func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) { Fingerprint: req.Fingerprint, PublicKey: req.SSHPublicKey, } - db.Where(models.SSHKey{Fingerprint: req.Fingerprint}).FirstOrCreate(&sshKey) + if err := db.Where(models.SSHKey{Fingerprint: req.Fingerprint}).FirstOrCreate(&sshKey).Error; err != nil { + // we specifically want to block users whos ssh key failed to save. this is to prevent downstream errors which i dont want to deal with now + // !TODO add a way to just log the errror and app continue without trying to save carts, cards and addresses. this way the users wouldnt + // just be kicked, they would shop normally but on their next login they would need to add all their data again. + utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to save ssh key", nil) + return + } } token, err := h.jwtManager.GenerateToken(user.ID, user.Email, user.Name) diff --git a/api/handlers/cards.go b/api/handlers/cards.go index e9a42b4..89ad643 100644 --- a/api/handlers/cards.go +++ b/api/handlers/cards.go @@ -119,10 +119,16 @@ func (h *CardHandler) SetDefaultCard(w http.ResponseWriter, r *http.Request) { return } - db.Model(&models.Card{}).Where("user_id = ?", userID).Update("is_default", false) + if err := db.Model(&models.Card{}).Where("user_id = ?", userID).Update("is_default", false).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to update card", nil) + return + } card.IsDefault = true - db.Save(&card) + if err := db.Save(&card).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to save card", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "card": card, @@ -285,7 +291,10 @@ func (h *CardHandler) DeleteCard(w http.ResponseWriter, r *http.Request) { // Clear the card from any cart that references it. cardID := uint(id) - db.Model(&models.Cart{}).Where("card_id = ? AND user_id = ?", cardID, userID).Update("card_id", nil) + if err := db.Model(&models.Cart{}).Where("card_id = ? AND user_id = ?", cardID, userID).Update("card_id", nil).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to clear card from cart", nil) + return + } // Detach from Stripe (best-effort; don't block deletion on Stripe errors). if len(card.StripePaymentID) >= 3 && card.StripePaymentID[:3] == "pm_" { @@ -294,7 +303,10 @@ func (h *CardHandler) DeleteCard(w http.ResponseWriter, r *http.Request) { } // Soft-delete the local record. - db.Delete(&card) + if err := db.Delete(&card).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete card", nil) + return + } audit.CardDeleted(userID, card.ID) @@ -325,6 +337,8 @@ func ensureStripeCustomer(gdb *gorm.DB, user *models.User) error { return err } user.StripeCustomerID = cust.ID - gdb.Save(user) + if err := gdb.Save(user).Error; err != nil { + return fmt.Errorf("Failed to save stripe customer: %w", err) + } return nil } diff --git a/api/handlers/cart.go b/api/handlers/cart.go index e54bfc2..dd6aaa4 100644 --- a/api/handlers/cart.go +++ b/api/handlers/cart.go @@ -5,10 +5,10 @@ import ( "fmt" "net/http" - "gorm.io/gorm" "github.com/stripe/stripe-go/v78" "github.com/stripe/stripe-go/v78/paymentintent" "github.com/stripe/stripe-go/v78/paymentmethod" + "gorm.io/gorm" "terminalShop/api/middleware" "terminalShop/pkg/audit" @@ -117,7 +117,10 @@ func (h *CartHandler) SetItem(w http.ResponseWriter, r *http.Request) { } if req.Quantity <= 0 { - db.Where("cart_id = ? AND coffee_id = ?", cart.ID, req.CoffeeID).Delete(&models.CartItem{}) + if err := db.Where("cart_id = ? AND coffee_id = ?", cart.ID, req.CoffeeID).Delete(&models.CartItem{}).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete cart item", nil) + return + } } else { var item models.CartItem result := db.Where("cart_id = ? AND coffee_id = ?", cart.ID, req.CoffeeID).First(&item) @@ -127,9 +130,15 @@ func (h *CartHandler) SetItem(w http.ResponseWriter, r *http.Request) { CoffeeID: req.CoffeeID, Quantity: req.Quantity, } - db.Create(&item) + if err := db.Create(&item).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to create item", nil) + return + } } else { - db.Model(&item).Update("quantity", req.Quantity) + if err := db.Model(&item).Update("quantity", req.Quantity).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to update quantity", nil) + return + } } } @@ -164,7 +173,10 @@ func (h *CartHandler) SetAddress(w http.ResponseWriter, r *http.Request) { return } - db.Model(cart).Update("address_id", req.AddressID) + if err := db.Model(cart).Update("address_id", req.AddressID).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to set address", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "message": "address set", @@ -197,7 +209,10 @@ func (h *CartHandler) SetCard(w http.ResponseWriter, r *http.Request) { return } - db.Model(cart).Update("card_id", req.CardID) + if err := db.Model(cart).Update("card_id", req.CardID).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to set card", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "message": "card set", @@ -214,7 +229,10 @@ func (h *CartHandler) ClearCart(w http.ResponseWriter, r *http.Request) { return } - db.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{}) + if err := db.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{}).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete cart item", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "message": "cart cleared", @@ -297,7 +315,10 @@ func (h *CartHandler) ConvertCart(w http.ResponseWriter, r *http.Request) { } paymentMethodID = pm.ID card.StripePaymentID = pm.ID - db.Save(&card) + if err := db.Save(&card).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to save card", nil) + return + } } var subtotal int @@ -427,10 +448,12 @@ func (h *CartHandler) ConvertCart(w http.ResponseWriter, r *http.Request) { audit.OrderPaid(userID, order.ID, total, pi.ID) - db.Preload("Items").First(&order, order.ID) + if err := db.Preload("Items").First(&order, order.ID).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to preload items", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "order": order, }) } - diff --git a/api/handlers/products.go b/api/handlers/products.go index 89baa61..1a6d95e 100644 --- a/api/handlers/products.go +++ b/api/handlers/products.go @@ -57,4 +57,3 @@ func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) { "product": product, }) } - diff --git a/api/handlers/ssh_keys.go b/api/handlers/ssh_keys.go index 2de4601..b97ac70 100644 --- a/api/handlers/ssh_keys.go +++ b/api/handlers/ssh_keys.go @@ -23,10 +23,7 @@ func (h *SSHKeyHandler) GetSSHKeys(w http.ResponseWriter, r *http.Request) { userID := middleware.UserIDFromContext(r.Context()) var keys []models.SSHKey - if err := db.Where("user_id = ?", userID).Order("created_at ASC").Find(&keys).Error; err != nil { - utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to fetch ssh keys", nil) - return - } + db.Where("user_id = ?", userID).Order("created_at ASC").Find(&keys) utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "ssh_keys": keys, @@ -36,7 +33,7 @@ func (h *SSHKeyHandler) GetSSHKeys(w http.ResponseWriter, r *http.Request) { // DeleteSSHKeys removes one of the users ssh keys by ID func (h *SSHKeyHandler) DeleteSSHKeys(w http.ResponseWriter, r *http.Request) { db := database.GetDB() - userId := middleware.UserIDFromContext(r.Context()) + userID := middleware.UserIDFromContext(r.Context()) idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) @@ -46,11 +43,14 @@ func (h *SSHKeyHandler) DeleteSSHKeys(w http.ResponseWriter, r *http.Request) { } var key models.SSHKey - if err := db.Where("id = ? AND user_id = ?", id, userId).First(&key).Error; err != nil { + if err := db.Where("id = ? AND user_id = ?", id, userID).First(&key).Error; err != nil { utils.RespondError(w, http.StatusNotFound, "SSH_KEY_NOT_FOUND", "ssh key not found", nil) return } - db.Delete(&key) + if err := db.Delete(&key).Error; err != nil { + utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete ssh key", nil) + return + } utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{ "message": "ssh key deleted", diff --git a/api/handlers/webhook.go b/api/handlers/webhook.go index c14f9eb..49a0975 100644 --- a/api/handlers/webhook.go +++ b/api/handlers/webhook.go @@ -2,6 +2,7 @@ package handlers import ( "encoding/json" + "fmt" "io" "log" "net/http" @@ -128,7 +129,9 @@ func (h *WebhookHandler) handlePaymentIntentSucceeded(event stripe.Event) { if err := tx.Where("user_id = ?", order.UserID).First(&cart).Error; err != nil { return nil // Cart already gone. } - tx.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{}) + if err := tx.Where("cart_id = ?", cart.ID).Delete(&models.CartItem{}).Error; err != nil { + return fmt.Errorf("failed to delete cart item: %w", err) + } return tx.Model(&cart).Updates(map[string]interface{}{ "address_id": nil, "card_id": nil, @@ -156,8 +159,10 @@ func (h *WebhookHandler) handlePaymentIntentFailed(event stripe.Event) { } db := database.GetDB() - db.Model(&models.Order{}).Where("id = ? AND status = ?", orderIDStr, models.OrderStatusPending). - Update("status", models.OrderStatusFailed) + if err := db.Model(&models.Order{}).Where("id = ? AND status = ?", orderIDStr, models.OrderStatusPending). + Update("status", models.OrderStatusFailed).Error; err != nil { + log.Printf("[webhook] failed to mark order %s as failed: %v", orderIDStr, err) + } errMsg := "unknown" if pi.LastPaymentError != nil { diff --git a/api/routes/routes.go b/api/routes/routes.go index fa9cb62..bcc05c3 100644 --- a/api/routes/routes.go +++ b/api/routes/routes.go @@ -117,7 +117,7 @@ func SetupRoutes( }) // SSH Keys - r.Route("/ssh-keys", func(r chi.Router){ + r.Route("/ssh-keys", func(r chi.Router) { // GET doesnt need id because it gets every ssh key r.Get("/", sshKeyHandler.GetSSHKeys) // Delete needs an id to delete only one key diff --git a/pkg/bring/bring.go b/pkg/bring/bring.go index 0d4f4bd..c41d431 100644 --- a/pkg/bring/bring.go +++ b/pkg/bring/bring.go @@ -42,7 +42,6 @@ type Address struct { Phone string } - type validationMatch struct { StreetName string `json:"street_name"` HouseNumber int `json:"house_number"` diff --git a/pkg/config/config.go b/pkg/config/config.go index a35c142..c672df5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -12,20 +12,20 @@ import ( // Config holds all application configuration type Config struct { - APIPort string - APIURL string - AppURL string - SSHPort string - Environment string - DatabaseURL string - JWTSecret string - StripeSecretKey string - StripePublicKey string - StripeWebhookSecret string - ShippoAPIKey string - AuthFingerprintKey string - BringAPIUID string - BringAPIKey string + APIPort string + APIURL string + AppURL string + SSHPort string + Environment string + DatabaseURL string + JWTSecret string + StripeSecretKey string + StripePublicKey string + StripeWebhookSecret string + ShippoAPIKey string + AuthFingerprintKey string + BringAPIUID string + BringAPIKey string } // Load reads configuration from environment variables and validates required secrets. diff --git a/pkg/models/cart.go b/pkg/models/cart.go index 60714dd..18699a3 100644 --- a/pkg/models/cart.go +++ b/pkg/models/cart.go @@ -11,7 +11,7 @@ type Cart struct { CardID *uint `json:"card_id,omitempty"` ShippingCost int `gorm:"default:0" json:"shipping_cost"` ShippingService string `gorm:"size:255" json:"shipping_service,omitempty"` - CreatedAt time.Time `json:"created_at"` + CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` User User `gorm:"foreignKey:UserID" json:"-"` diff --git a/pkg/tui/menu.go b/pkg/tui/menu.go index 73bed7d..1b2c695 100644 --- a/pkg/tui/menu.go +++ b/pkg/tui/menu.go @@ -2,17 +2,16 @@ package tui import ( "fmt" - "github.com/charmbracelet/lipgloss" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) - func (m Model) BuildMenuView() string { bold := m.theme.TextAccent().Bold(true) base := m.theme.TextBody() - rowStyle := lipgloss.NewStyle().Padding(0,1) + rowStyle := lipgloss.NewStyle().Padding(0, 1) rows := []string{ rowStyle.Render(bold.Render("s") + " " + base.Render("shop")), rowStyle.Render(bold.Render("a") + " " + base.Render("account")), @@ -21,13 +20,11 @@ func (m Model) BuildMenuView() string { rowStyle.Render(bold.Render("q") + " " + base.Render("quit")), } - menuContent := "" for _, row := range rows { menuContent += row + "\n" } - logoStyle := m.theme.TextAccent().Bold(true) logo := logoStyle.Render("terminal coffee") @@ -52,7 +49,7 @@ func (m Model) BuildMenuView() string { "", hint, cartHint, - ) + ) return lipgloss.Place( m.viewportWidth, @@ -60,8 +57,8 @@ func (m Model) BuildMenuView() string { lipgloss.Center, lipgloss.Center, assembled, - ) - + ) + } func (m Model) MenuUpdate(msg tea.Msg) (Model, tea.Cmd) { diff --git a/pkg/tui/qrfefe/qr.go b/pkg/tui/qrfefe/qr.go index 7f5388b..57b885e 100644 --- a/pkg/tui/qrfefe/qr.go +++ b/pkg/tui/qrfefe/qr.go @@ -20,7 +20,7 @@ func generate(level qr.Level, text string) (string, int, error) { qrRunes := make([]rune, 0) - for y := 0; y < code.Size-1; y+= 2 { + for y := 0; y < code.Size-1; y += 2 { qrRunes = append(qrRunes, []rune(sgr.FgWhite+sgr.BgBlack)...) for x := 0; x < code.Size; x += 1 { diff --git a/pkg/tui/shop.go b/pkg/tui/shop.go index 578c15a..52ec23f 100644 --- a/pkg/tui/shop.go +++ b/pkg/tui/shop.go @@ -83,9 +83,9 @@ func (m Model) getShopMenuContent() string { Background(selectedColor).Bold(true) } else { normal = m.theme.TextBody(). - Width(menuWidth + 2).Padding(0, 1) + Width(menuWidth+2).Padding(0, 1) highlighted = m.theme.TextAccent(). - Width(menuWidth + 2).Padding(0, 1). + Width(menuWidth+2).Padding(0, 1). Background(selectedColor).Bold(true) }