feat: add ssh key management to account page
This commit is contained in:
47
NEXT.md
Normal file
47
NEXT.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Next Session
|
||||
|
||||
## Feature: SSH Key Management in Account Page
|
||||
|
||||
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.
|
||||
|
||||
### What needs doing
|
||||
|
||||
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`
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
5. **`pkg/tui/update.go`** — fetch SSH keys when switching to account page
|
||||
(same as orders).
|
||||
|
||||
### 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).
|
||||
|
||||
### 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`,
|
||||
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`
|
||||
@@ -72,7 +72,7 @@ func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
user = models.User{
|
||||
SSHKeyFingerprint: req.Fingerprint,
|
||||
SSHPublicKey: req.SSHPublicKey,
|
||||
SSHPublicKey: req.SSHPublicKey,
|
||||
}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to create user", nil)
|
||||
@@ -83,6 +83,15 @@ func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.SSHPublicKey != "" {
|
||||
sshKey := models.SSHKey{
|
||||
UserID: user.ID,
|
||||
Fingerprint: req.Fingerprint,
|
||||
PublicKey: req.SSHPublicKey,
|
||||
}
|
||||
db.Where(models.SSHKey{Fingerprint: req.Fingerprint}).FirstOrCreate(&sshKey)
|
||||
}
|
||||
|
||||
token, err := h.jwtManager.GenerateToken(user.ID, user.Email, user.Name)
|
||||
if err != nil {
|
||||
utils.RespondError(w, http.StatusInternalServerError, "TOKEN_ERROR", "failed to generate token", nil)
|
||||
@@ -93,7 +102,7 @@ func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) {
|
||||
"access_token": token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 1800, // 30 minutes in seconds
|
||||
"user": user.ToPublic(),
|
||||
"user": user.ToPublic(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
58
api/handlers/ssh_keys.go
Normal file
58
api/handlers/ssh_keys.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"terminalShop/api/middleware"
|
||||
"terminalShop/pkg/database"
|
||||
"terminalShop/pkg/models"
|
||||
"terminalShop/pkg/utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type SSHKeyHandler struct{}
|
||||
|
||||
func NewSSHKeyHandler() *SSHKeyHandler {
|
||||
return &SSHKeyHandler{}
|
||||
}
|
||||
|
||||
// GetSSHKeys returns all ssh keys for the authenticated user.
|
||||
func (h *SSHKeyHandler) GetSSHKeys(w http.ResponseWriter, r *http.Request) {
|
||||
db := database.GetDB()
|
||||
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
|
||||
}
|
||||
|
||||
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
|
||||
"ssh_keys": keys,
|
||||
})
|
||||
}
|
||||
|
||||
// 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())
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondError(w, http.StatusBadRequest, "INVALID_ID", "invalid ssh key id", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var key models.SSHKey
|
||||
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)
|
||||
|
||||
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "ssh key deleted",
|
||||
})
|
||||
}
|
||||
@@ -37,6 +37,9 @@ func (h *ViewHandler) GetViewInit(w http.ResponseWriter, r *http.Request) {
|
||||
var cards []models.Card
|
||||
db.Where("user_id = ?", userID).Find(&cards)
|
||||
|
||||
var sshKeys []models.SSHKey
|
||||
db.Where("user_id = ?", userID).Order("created_at ASC").Find(&sshKeys)
|
||||
|
||||
var orders []models.Order
|
||||
db.Where("user_id = ?", userID).Preload("Items").Order("created_at desc").Find(&orders)
|
||||
var cartItems []models.CartItem
|
||||
@@ -50,6 +53,7 @@ func (h *ViewHandler) GetViewInit(w http.ResponseWriter, r *http.Request) {
|
||||
"cart": cartItems,
|
||||
"addresses": addresses,
|
||||
"cards": cards,
|
||||
"ssh_keys": sshKeys,
|
||||
"orders": orders,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ func SetupRoutes(
|
||||
addressHandler := handlers.NewAddressHandler(shippoAPIKey, bringAPIUID, bringAPIKey)
|
||||
viewHandler := handlers.NewViewHandler()
|
||||
webhookHandler := handlers.NewWebhookHandler(stripeWebhookSecret, stripeSecretKey)
|
||||
sshKeyHandler := handlers.NewSSHKeyHandler()
|
||||
|
||||
// Short payment redirect and success page — no auth required.
|
||||
r.Get("/pay/{token}", handlers.PayRedirect)
|
||||
@@ -114,6 +115,14 @@ func SetupRoutes(
|
||||
r.Delete("/{id}", addressHandler.DeleteAddress)
|
||||
r.Put("/{id}/default", addressHandler.SetDefaultAddress)
|
||||
})
|
||||
|
||||
// SSH Keys
|
||||
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
|
||||
r.Delete("/{id}", sshKeyHandler.DeleteSSHKeys)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -628,6 +628,64 @@ func (c *Client) DeleteCard(id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type SSHKeyResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
SSHKeys []models.SSHKey `json:"ssh_keys"`
|
||||
} `json:"data"`
|
||||
Error *APIError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) GetSSHKeys() ([]models.SSHKey, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/ssh-keys", c.BaseURL)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch ssh keys: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var keysResp SSHKeyResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&keysResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode ssh keys response: %w", err)
|
||||
}
|
||||
|
||||
if !keysResp.Success {
|
||||
if keysResp.Error != nil {
|
||||
return nil, fmt.Errorf("%s: %s", keysResp.Error.Code, keysResp.Error.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to fetch ssh keys")
|
||||
}
|
||||
return keysResp.Data.SSHKeys, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) DeleteSSHKey(id uint) error {
|
||||
url := fmt.Sprintf("%s/api/v1/ssh-keys/%d", c.BaseURL, id)
|
||||
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create delete request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete ssh key: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("delete ssh key failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type OrderResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
@@ -784,6 +842,7 @@ type ViewInitData struct {
|
||||
Cart []CartItemData `json:"cart"`
|
||||
Addresses []models.Address `json:"addresses"`
|
||||
Cards []models.Card `json:"cards"`
|
||||
SSHKeys []models.SSHKey `json:"ssh_keys"`
|
||||
Orders []models.Order `json:"orders"`
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ func Migrate(db *gorm.DB) error {
|
||||
&models.Order{}, // Completed purchases
|
||||
&models.OrderItem{}, // Line items within orders
|
||||
&models.Address{}, // Saved addresses
|
||||
&models.SSHKey{}, // One of the users ssh keys
|
||||
); err != nil {
|
||||
return fmt.Errorf("migration failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,12 @@ func (Coffee) TableName() string {
|
||||
return "coffees"
|
||||
}
|
||||
|
||||
// AccountMenuItems
|
||||
// AccountMenuItems
|
||||
var AccountMenuItems = []string{
|
||||
"order history",
|
||||
"addresses",
|
||||
"cards",
|
||||
"ssh keys",
|
||||
"faq",
|
||||
"about",
|
||||
}
|
||||
|
||||
28
pkg/models/ssh_key.go
Normal file
28
pkg/models/ssh_key.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SSHKey represents one of a users registered SSH public keys.
|
||||
// A user may have multiple keys, any of them grants access.
|
||||
type SSHKey struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
Fingerprint string `gorm:"uniqueIndex;size:100;not null" json:"fingerprint"`
|
||||
PublicKey string `gorm:"type:text" json:"-"`
|
||||
Comment string `gorm:"size:255" json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// Associations
|
||||
User User `gorm:"foreignKey:UserID" json:"-"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for the SSHKey model.
|
||||
func (SSHKey) TableName() string {
|
||||
return "ssh_keys"
|
||||
}
|
||||
@@ -127,6 +127,39 @@ func (m Model) BuildAccountView(availableHeight int) string {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
detailView = lines
|
||||
|
||||
case "ssh keys":
|
||||
lines := titleStyle.Render("SSH keys") + "\n\n"
|
||||
if !m.SSHKeysLoaded {
|
||||
lines += contentStyle.Render("Loading ssh keys...")
|
||||
} else if len(m.SSHKeys) == 0 {
|
||||
lines += contentStyle.Render("No saved ssh keys.")
|
||||
} else {
|
||||
for i, key := range m.SSHKeys {
|
||||
isSelected := m.SSHKeyListFocused && i == m.AccountSSHKeyCursor
|
||||
label := key.Fingerprint
|
||||
if key.Comment != "" {
|
||||
label += " " + key.Comment
|
||||
}
|
||||
if len(label) > detailContentWidth-4 {
|
||||
label = label[:detailContentWidth-4]
|
||||
}
|
||||
if m.AccountSSHKeyDeleting != nil && *m.AccountSSHKeyDeleting == i {
|
||||
lines += m.theme.TextError().Bold(true).Render(" delete? (y/n)") + "\n"
|
||||
} else if isSelected {
|
||||
lines += m.theme.TextAccent().Bold(true).Render("> "+label) + "\n"
|
||||
} else {
|
||||
lines += contentStyle.Render(" "+label) + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.SSHKeyListFocused {
|
||||
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
|
||||
} else {
|
||||
lines += "\n" + m.theme.TextDim().Render("enter: manage")
|
||||
}
|
||||
detailView = lines
|
||||
|
||||
case "faq":
|
||||
questionStyle := m.theme.TextHighlight().Bold(true)
|
||||
|
||||
@@ -439,6 +472,10 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if m.AccountCardCursor > 0 {
|
||||
m.AccountCardCursor--
|
||||
}
|
||||
} else if m.SSHKeyListFocused && m.AccountSSHKeyDeleting == nil {
|
||||
if m.AccountSSHKeyCursor > 0 {
|
||||
m.AccountSSHKeyCursor--
|
||||
}
|
||||
} else if m.FaqFocused {
|
||||
m.ScrollOffset -= 3
|
||||
if m.ScrollOffset < 0 {
|
||||
@@ -468,6 +505,10 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if m.AccountCardCursor < len(m.SavedCards)-1 {
|
||||
m.AccountCardCursor++
|
||||
}
|
||||
} else if m.SSHKeyListFocused && m.AccountSSHKeyDeleting == nil {
|
||||
if m.AccountSSHKeyCursor < len(m.SSHKeys)-1 {
|
||||
m.AccountSSHKeyCursor++
|
||||
}
|
||||
} else if m.FaqFocused {
|
||||
m.ScrollOffset += 3
|
||||
maxScroll := m.computeFaqScrollMax()
|
||||
@@ -491,7 +532,7 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if targetBottom > m.ScrollOffset+viewportHeight {
|
||||
m.ScrollOffset = targetBottom - viewportHeight
|
||||
}
|
||||
} else if m.OrderViewState == 0 && !m.AddressListFocused && !m.CardListFocused && m.AccountCursor < len(models.AccountMenuItems)-1 {
|
||||
} else if m.OrderViewState == 0 && !m.AddressListFocused && !m.CardListFocused && !m.SSHKeyListFocused && m.AccountCursor < len(models.AccountMenuItems)-1 {
|
||||
m.AccountCursor++
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
@@ -518,6 +559,8 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.AccountAddressDeleting = &m.AccountAddressCursor
|
||||
} else if m.CardListFocused && m.AccountCardDeleting == nil && m.AccountCardCursor < len(m.SavedCards) {
|
||||
m.AccountCardDeleting = &m.AccountCardCursor
|
||||
} else if m.SSHKeyListFocused && m.AccountSSHKeyDeleting == nil && m.AccountSSHKeyCursor < len(m.SSHKeys) {
|
||||
m.AccountSSHKeyDeleting = &m.AccountSSHKeyCursor
|
||||
}
|
||||
|
||||
case "y":
|
||||
@@ -539,11 +582,21 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.AccountCardCursor--
|
||||
}
|
||||
return m, m.deleteCardCmd(card.ID)
|
||||
} else if m.AccountSSHKeyDeleting != nil {
|
||||
cursor := *m.AccountSSHKeyDeleting
|
||||
key := m.SSHKeys[cursor]
|
||||
m.AccountSSHKeyDeleting = nil
|
||||
m.SSHKeys = append(m.SSHKeys[:cursor], m.SSHKeys[cursor+1:]...)
|
||||
if m.AccountSSHKeyCursor >= len(m.SSHKeys) && m.AccountSSHKeyCursor > 0 {
|
||||
m.AccountSSHKeyCursor--
|
||||
}
|
||||
return m, m.deleteSSHKeyCmd(key.ID)
|
||||
}
|
||||
|
||||
case "n":
|
||||
m.AccountAddressDeleting = nil
|
||||
m.AccountCardDeleting = nil
|
||||
m.AccountSSHKeyDeleting = nil
|
||||
|
||||
case "p", "enter":
|
||||
switch selectedItem {
|
||||
@@ -558,6 +611,11 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.ScrollOffset = 0
|
||||
}
|
||||
}
|
||||
case "ssh keys":
|
||||
if !m.SSHKeyListFocused {
|
||||
m.SSHKeyListFocused = true
|
||||
m.AccountSSHKeyCursor = 0
|
||||
}
|
||||
case "addresses":
|
||||
if !m.AddressListFocused {
|
||||
m.AddressListFocused = true
|
||||
@@ -586,6 +644,11 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
} else if m.CardListFocused {
|
||||
m.CardListFocused = false
|
||||
m.AccountCardCursor = 0
|
||||
} else if m.AccountSSHKeyDeleting != nil {
|
||||
m.AccountSSHKeyDeleting = nil
|
||||
} else if m.SSHKeyListFocused {
|
||||
m.SSHKeyListFocused = false
|
||||
m.AccountSSHKeyCursor = 0
|
||||
} else if m.FaqFocused {
|
||||
m.FaqFocused = false
|
||||
m.ScrollOffset = 0
|
||||
|
||||
@@ -140,6 +140,13 @@ type Model struct {
|
||||
AccountAddressDeleting *int // index of address pending deletion
|
||||
AccountCardDeleting *int // index of card pending deletion
|
||||
|
||||
// Account SSH key state
|
||||
SSHKeys []models.SSHKey // list of keys
|
||||
SSHKeysLoaded bool // true when ssh keys are loaded
|
||||
SSHKeyListFocused bool // true when browsing ssh keys in account
|
||||
AccountSSHKeyCursor int // cursor within ssh key list in account
|
||||
AccountSSHKeyDeleting *int // index of ssh key pending deletion
|
||||
|
||||
// Splash screen
|
||||
splashDataReady bool // true when products have loaded
|
||||
splashDelayDone bool // true when minimum display time has elapsed
|
||||
@@ -285,6 +292,8 @@ func (m Model) resetPageState() Model {
|
||||
m.CardListFocused = false
|
||||
m.AccountAddressDeleting = nil
|
||||
m.AccountCardDeleting = nil
|
||||
m.SSHKeyListFocused = false
|
||||
m.AccountSSHKeyDeleting = nil
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -390,6 +399,17 @@ type CardDeletedMsg struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// SSHKeysMsg is sent when ssh keys are fetched from the API
|
||||
type SSHKeysMsg struct {
|
||||
Keys []models.SSHKey
|
||||
Err error
|
||||
}
|
||||
|
||||
// SSHKeyDeletedMsg is sent when an ssh key is deleted
|
||||
type SSHKeyDeletedMsg struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (m Model) fetchAddressesCmd() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.APIClient == nil || m.User == nil {
|
||||
@@ -452,6 +472,26 @@ func (m Model) deleteCardCmd(id uint) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) fetchSSHKeysCmd() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.APIClient == nil || m.User == nil {
|
||||
return SSHKeysMsg{Err: fmt.Errorf("not authenticated")}
|
||||
}
|
||||
keys, err := m.APIClient.GetSSHKeys()
|
||||
return SSHKeysMsg{Keys: keys, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) deleteSSHKeyCmd(id uint) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if m.APIClient == nil || m.User == nil {
|
||||
return SSHKeyDeletedMsg{Err: fmt.Errorf("not authenticated")}
|
||||
}
|
||||
err := m.APIClient.DeleteSSHKey(id)
|
||||
return SSHKeyDeletedMsg{Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
// checkoutWithSavedCard sets address and saved card on the cart, then converts.
|
||||
// Used when the user selects an existing card from the list (no tokenization needed).
|
||||
func (m Model) checkoutWithSavedCard() tea.Cmd {
|
||||
|
||||
@@ -89,6 +89,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.loadCartFromAPI(cart)
|
||||
m.SavedAddresses = msg.Data.Addresses
|
||||
m.SavedCards = msg.Data.Cards
|
||||
m.SSHKeys = msg.Data.SSHKeys
|
||||
m.SSHKeysLoaded = true
|
||||
m.Orders = msg.Data.Orders
|
||||
m.OrdersLoaded = true
|
||||
m.splashDataReady = true
|
||||
@@ -133,6 +135,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.ErrorMsg = fmt.Sprintf("failed to delete card: %v", msg.Err)
|
||||
}
|
||||
return m, nil
|
||||
case SSHKeysMsg:
|
||||
if msg.Err == nil {
|
||||
m.SSHKeys = msg.Keys
|
||||
m.SSHKeysLoaded = true
|
||||
}
|
||||
return m, nil
|
||||
case SSHKeyDeletedMsg:
|
||||
if msg.Err != nil {
|
||||
m.ErrorMsg = fmt.Sprintf("failed to delete ssh key: %v", msg.Err)
|
||||
}
|
||||
return m, nil
|
||||
case DelayCompleteMsg:
|
||||
m.splashDelayDone = true
|
||||
if m.currentPage == splashPage && m.splashDataReady {
|
||||
|
||||
Reference in New Issue
Block a user