feat: add "forgot account" reset flow with wallet preservation

Add account reset functionality for users who forgot their login credentials.
The reset clears authentication data while preserving wallet private keys and
exchange configs, which are automatically adopted by the new account on
re-registration to prevent fund loss.

- Add POST /api/reset-account endpoint
- Add "Forgot account?" button on login page (zh/en/id)
- Orphan ai_models and exchanges are re-assigned to new user on register
- Onboarding reuses existing claw402 wallet instead of generating new one

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
shinchan-zhai
2026-04-10 18:00:56 +08:00
parent 80272c0d5a
commit 4f0a922779
7 changed files with 133 additions and 0 deletions

View File

@@ -102,6 +102,10 @@ func (s *Server) handleRegister(c *gin.Context) {
return
}
// Adopt orphan records from previous account (e.g. after account reset)
// This preserves wallet keys and exchange configs so funds are not lost.
s.adoptOrphanRecords(userID)
// Generate JWT token
token, err := auth.GenerateJWT(user.ID, user.Email)
if err != nil {
@@ -222,6 +226,50 @@ func (s *Server) handleResetPassword(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Password reset successful, please login with new password"})
}
// handleResetAccount clears user authentication data so the system returns to
// uninitialized state for re-registration. Wallet keys (ai_models) are preserved
// so funds are not lost — they will be adopted by the new account during onboarding.
func (s *Server) handleResetAccount(c *gin.Context) {
err := s.store.Transaction(func(tx *gorm.DB) error {
// Delete traders and strategies (config, not funds)
tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&store.Trader{})
tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&store.Strategy{})
// Delete users — ai_models and exchanges are intentionally kept
// so wallet private keys and exchange configs survive re-registration
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&store.User{}).Error; err != nil {
return fmt.Errorf("failed to delete users: %w", err)
}
return nil
})
if err != nil {
SafeInternalError(c, "Failed to reset account", err)
return
}
logger.Infof("✓ User accounts cleared (wallets preserved) — system reset to uninitialized")
c.JSON(http.StatusOK, gin.H{"message": "Account reset successful, you can now register a new account"})
}
// adoptOrphanRecords re-assigns ai_models and exchanges whose user_id no longer
// exists in the users table. This happens after account reset so the new user
// inherits the previous wallet keys and exchange configurations.
func (s *Server) adoptOrphanRecords(newUserID string) {
db := s.store.GormDB()
result := db.Model(&store.AIModel{}).
Where("user_id NOT IN (SELECT id FROM users)").
Update("user_id", newUserID)
if result.RowsAffected > 0 {
logger.Infof("✓ Adopted %d orphan ai_model(s) for new user %s", result.RowsAffected, newUserID)
}
result = db.Model(&store.Exchange{}).
Where("user_id NOT IN (SELECT id FROM users)").
Update("user_id", newUserID)
if result.RowsAffected > 0 {
logger.Infof("✓ Adopted %d orphan exchange(s) for new user %s", result.RowsAffected, newUserID)
}
}
// initUserDefaultConfigs Initialize default configs for new user
func (s *Server) initUserDefaultConfigs(userID string, lang string) error {
if err := s.createDefaultStrategies(userID, lang); err != nil {