resolve splash screen deadlock and auth 500 soft-deleted ssh keys
Some checks failed
Deploy / deploy (push) Has been cancelled

Auth handler used FirstOrCreate which couldn't see soft-deleted keys
  (GORM filters by deleted_at IS NULL) but also couldn't insert (unique
  index still covers deleted rows), returning 500 on every login after
  a key deletion. Use Unscoped lookup and restore soft-deleted keys.

  Splash screen also got permanently stuck when auth failed because
  splashViewInitCmd was never fired, leaving splash.dataReady false.
This commit is contained in:
2026-04-24 16:51:19 +02:00
parent d66b41eed3
commit d7b172843e
2 changed files with 28 additions and 15 deletions

View File

@@ -84,17 +84,30 @@ func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) {
}
if req.SSHPublicKey != "" {
sshKey := models.SSHKey{
UserID: user.ID,
Fingerprint: req.Fingerprint,
PublicKey: req.SSHPublicKey,
}
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
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
}
}
}

View File

@@ -58,7 +58,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case SplashAuthMsg:
if msg.Err != nil {
m.ErrorMsg = fmt.Sprintf("authentication failed: %v", msg.Err)
return m, nil
// Still attempt to load view data (products may not require auth)
// so the splash screen doesn't get permanently stuck.
return m, m.splashViewInitCmd
}
m.AccessToken = msg.Token
m.APIClient.Token = msg.Token
@@ -93,9 +95,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.OrdersLoaded = true
m.splash.dataReady = true
if m.splash.delayDone {
m = m.SwitchPage(shopPage)
m = m.updateShopViewports()
return m, nil
return m.ShopSwitch()
}
return m, nil
case CartSyncedMsg: