refactor: remove putting ssh key in memory at all, just use it for auth

changed the ssh key to just do fingerprinting and not store it at all, and not be able to edit it either in the account page
This commit is contained in:
2026-04-25 10:50:19 +02:00
parent c8af9cb551
commit c78f5eb12b
10 changed files with 1 additions and 230 deletions

View File

@@ -83,34 +83,6 @@ func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) {
return
}
if req.SSHPublicKey != "" {
var sshKey models.SSHKey
// Use Unscoped to include soft-deleted rows, avoiding the
// UNIQUE constraint conflict when a key is re-registered.
err := db.Unscoped().Where("fingerprint = ?", req.Fingerprint).First(&sshKey).Error
if err != nil {
// Key doesn't exist at all — create it
sshKey = models.SSHKey{
UserID: user.ID,
Fingerprint: req.Fingerprint,
PublicKey: req.SSHPublicKey,
}
if err := db.Create(&sshKey).Error; err != nil {
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to save ssh key", nil)
return
}
} else if sshKey.DeletedAt.Valid {
// Key was soft-deleted — restore it
sshKey.DeletedAt = gorm.DeletedAt{}
sshKey.PublicKey = req.SSHPublicKey
sshKey.UserID = user.ID
if err := db.Unscoped().Save(&sshKey).Error; err != nil {
utils.RespondError(w, http.StatusInternalServerError, "DATABASE_ERROR", "failed to restore ssh key", nil)
return
}
}
}
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)

View File

@@ -37,9 +37,6 @@ 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
@@ -53,7 +50,6 @@ func (h *ViewHandler) GetViewInit(w http.ResponseWriter, r *http.Request) {
"cart": cartItems,
"addresses": addresses,
"cards": cards,
"ssh_keys": sshKeys,
"orders": orders,
})
}

View File

@@ -43,7 +43,6 @@ 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)
@@ -115,14 +114,6 @@ 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)
})
})
})

View File

@@ -628,64 +628,6 @@ 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 {
@@ -842,7 +784,6 @@ 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"`
}

View File

@@ -23,7 +23,6 @@ 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)
}

View File

@@ -31,7 +31,6 @@ var AccountMenuItems = []string{
"order history",
"addresses",
"cards",
"ssh keys",
"faq",
"about",
}

View File

@@ -49,8 +49,6 @@ func (m Model) BuildAccountView(availableHeight int) string {
detailView = m.AddressesView(detailContentWidth)
case "cards":
detailView = m.CardsView(detailContentWidth)
case "ssh keys":
detailView = m.SSHKeysView(detailContentWidth)
case "faq":
questionStyle := m.theme.TextHighlight().Bold(true)
faqContent := ""
@@ -222,10 +220,6 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
if m.account.cardCursor > 0 {
m.account.cardCursor--
}
} else if m.account.sshKeyListFocused && m.account.sshKeyDeleting == nil {
if m.account.sshKeyCursor > 0 {
m.account.sshKeyCursor--
}
} else if m.account.faqFocused {
m.account.scrollOffset -= 3
if m.account.scrollOffset < 0 {
@@ -255,10 +249,6 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
if m.account.cardCursor < len(m.SavedCards)-1 {
m.account.cardCursor++
}
} else if m.account.sshKeyListFocused && m.account.sshKeyDeleting == nil {
if m.account.sshKeyCursor < len(m.SSHKeys)-1 {
m.account.sshKeyCursor++
}
} else if m.account.faqFocused {
m.account.scrollOffset += 3
maxScroll := m.computeFaqScrollMax()
@@ -282,7 +272,7 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
if targetBottom > m.account.scrollOffset+viewportHeight {
m.account.scrollOffset = targetBottom - viewportHeight
}
} else if m.account.orderViewState == 0 && !m.account.addressListFocused && !m.account.cardListFocused && !m.account.sshKeyListFocused && m.account.cursor < len(models.AccountMenuItems)-1 {
} else if m.account.orderViewState == 0 && !m.account.addressListFocused && !m.account.cardListFocused && m.account.cursor < len(models.AccountMenuItems)-1 {
m.account.cursor++
m.account.scrollOffset = 0
}
@@ -309,8 +299,6 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
m.account.addressDeleting = &m.account.addressCursor
} else if m.account.cardListFocused && m.account.cardDeleting == nil && m.account.cardCursor < len(m.SavedCards) {
m.account.cardDeleting = &m.account.cardCursor
} else if m.account.sshKeyListFocused && m.account.sshKeyDeleting == nil && m.account.sshKeyCursor < len(m.SSHKeys) {
m.account.sshKeyDeleting = &m.account.sshKeyCursor
}
case "y":
@@ -332,21 +320,11 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
m.account.cardCursor--
}
return m, m.deleteCardCmd(card.ID)
} else if m.account.sshKeyDeleting != nil {
cursor := *m.account.sshKeyDeleting
key := m.SSHKeys[cursor]
m.account.sshKeyDeleting = nil
m.SSHKeys = append(m.SSHKeys[:cursor], m.SSHKeys[cursor+1:]...)
if m.account.sshKeyCursor >= len(m.SSHKeys) && m.account.sshKeyCursor > 0 {
m.account.sshKeyCursor--
}
return m, m.deleteSSHKeyCmd(key.ID)
}
case "n":
m.account.addressDeleting = nil
m.account.cardDeleting = nil
m.account.sshKeyDeleting = nil
case "p", "enter":
switch selectedItem {
@@ -372,16 +350,6 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
}
}
}
case "ssh keys":
if !m.account.sshKeyListFocused {
m.account.sshKeyListFocused = true
m.account.sshKeyCursor = 0
m.footer = []footerCommand{
{key: "j/k", value: "keys"},
{key: "x", value: "delete"},
{key: "esc", value: "back"},
}
}
case "addresses":
if !m.account.addressListFocused {
m.account.addressListFocused = true
@@ -437,11 +405,6 @@ func (m Model) AccountUpdate(msg tea.Msg) (Model, tea.Cmd) {
m.account.cardListFocused = false
m.account.cardCursor = 0
m.footer = accountDefaultFooter
} else if m.account.sshKeyDeleting != nil {
m.account.sshKeyDeleting = nil
} else if m.account.sshKeyListFocused {
m.account.sshKeyListFocused = false
m.account.sshKeyCursor = 0
m.footer = accountDefaultFooter
} else if m.account.faqFocused {
m.account.faqFocused = false

View File

@@ -1,41 +0,0 @@
package tui
func (m Model) SSHKeysView(width int) string {
titleSTyle := m.theme.TextAccent().Bold(true).MarginBottom(2)
contentStyle := m.theme.TextBody().Width(width)
activeStyle := m.theme.TextAccent().Bold(true)
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.account.sshKeyListFocused && i == m.account.sshKeyCursor
label := key.Fingerprint
if key.Comment != "" {
label += " " + key.Comment
}
if len(label) > width-4 {
label += label[:width-4]
}
if m.account.sshKeyDeleting != nil && *m.account.sshKeyDeleting == i {
lines += m.theme.TextError().Bold(true).Render(" delete? (y/n)") + "\n"
} else if isSelected {
lines += activeStyle.Render("> "+label) + "\n"
} else {
lines += contentStyle.Render(" "+label) + "\n"
}
}
}
if m.account.sshKeyListFocused {
lines += "\n" + m.theme.TextDim().Render("x: delete esc: back")
} else {
lines += "\n" + m.theme.TextDim().Render("enter: manage")
}
return lines
}

View File

@@ -77,8 +77,6 @@ type Model struct {
SavedCards []models.Card
Orders []models.Order
OrdersLoaded bool
SSHKeys []models.SSHKey
SSHKeysLoaded bool
FAQs []FAQ
// Page navigation
@@ -180,9 +178,6 @@ type accountState struct {
cardCursor int
addressDeleting *int
cardDeleting *int
sshKeyListFocused bool
sshKeyCursor int
sshKeyDeleting *int
}
type reviewState struct {
@@ -519,17 +514,6 @@ 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 {
@@ -592,26 +576,6 @@ 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 {

View File

@@ -89,8 +89,6 @@ 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.splash.dataReady = true
@@ -133,17 +131,6 @@ 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.splash.delayDone = true
if m.currentPage == splashPage && m.splash.dataReady {