mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
feat: forgot account reset flow + frontend default model fix to GLM
- Add forgot account reset flow with wallet preservation - Update frontend default model references from DeepSeek to GLM - Add reset-account API endpoint - Add orphan record adoption for wallet/exchange preservation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,7 @@ func (s *Server) handleCurrentBeginnerWallet(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (s *Server) resolveBeginnerWallet(userID string) (privateKey string, address string, configuredModelID string, reused bool, err error) {
|
||||
// 1. Check if current user already has a claw402 wallet
|
||||
models, err := s.store.AIModel().List(userID)
|
||||
if err != nil {
|
||||
return "", "", "", false, err
|
||||
@@ -175,6 +176,25 @@ func (s *Server) resolveBeginnerWallet(userID string) (privateKey string, addres
|
||||
return existingKey, addr, model.ID, true, nil
|
||||
}
|
||||
|
||||
// 2. Check for orphan claw402 wallet from a previous account (e.g. after account reset).
|
||||
// Adopt it to preserve funds.
|
||||
orphan, orphanErr := s.store.AIModel().FindOrphanClaw402()
|
||||
if orphanErr == nil && orphan != nil {
|
||||
existingKey := strings.TrimSpace(orphan.APIKey.String())
|
||||
if existingKey != "" {
|
||||
addr, addrErr := walletAddressFromPrivateKey(existingKey)
|
||||
if addrErr == nil {
|
||||
if adoptErr := s.store.AIModel().AdoptModel(orphan.ID, userID); adoptErr != nil {
|
||||
logger.Warnf("Failed to adopt orphan claw402 wallet for user %s: %v", userID, adoptErr)
|
||||
} else {
|
||||
logger.Infof("✓ Adopted orphan claw402 wallet %s for new user %s (address: %s)", orphan.ID, userID, addr)
|
||||
return existingKey, addr, orphan.ID, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. No existing wallet found — generate a new one
|
||||
privateKeyObj, genErr := gethcrypto.GenerateKey()
|
||||
if genErr != nil {
|
||||
return "", "", "", false, genErr
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -118,6 +118,7 @@ func (s *Server) setupRoutes() {
|
||||
s.route(api, "POST", "/register", "Register new user", s.handleRegister)
|
||||
s.route(api, "POST", "/login", "User login, returns JWT token", s.handleLogin)
|
||||
s.route(api, "POST", "/reset-password", "Reset password", s.handleResetPassword)
|
||||
s.route(api, "POST", "/reset-account", "Clear all users and reset system to allow re-registration", s.handleResetAccount)
|
||||
|
||||
// Routes requiring authentication
|
||||
protected := api.Group("/", s.authMiddleware())
|
||||
|
||||
@@ -54,6 +54,24 @@ func (s *AIModelStore) initDefaultData() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindOrphanClaw402 finds a claw402 model whose user_id no longer exists in the users table.
|
||||
// Used to recover wallets after account reset.
|
||||
func (s *AIModelStore) FindOrphanClaw402() (*AIModel, error) {
|
||||
var model AIModel
|
||||
err := s.db.Where("provider = ? AND api_key != '' AND user_id NOT IN (SELECT id FROM users)", "claw402").
|
||||
First(&model).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model, nil
|
||||
}
|
||||
|
||||
// AdoptModel re-assigns an existing model to a new user.
|
||||
func (s *AIModelStore) AdoptModel(modelID, newUserID string) error {
|
||||
return s.db.Model(&AIModel{}).Where("id = ?", modelID).
|
||||
Update("user_id", newUserID).Error
|
||||
}
|
||||
|
||||
// List retrieves user's AI model list
|
||||
func (s *AIModelStore) List(userID string) ([]*AIModel, error) {
|
||||
var models []*AIModel
|
||||
|
||||
@@ -112,6 +112,11 @@ func (s *UserStore) UpdatePassword(userID, passwordHash string) error {
|
||||
}).Error
|
||||
}
|
||||
|
||||
// DeleteAll deletes all users (reset system to uninitialized state)
|
||||
func (s *UserStore) DeleteAll() error {
|
||||
return s.db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{}).Error
|
||||
}
|
||||
|
||||
// EnsureAdmin ensures admin user exists
|
||||
func (s *UserStore) EnsureAdmin() error {
|
||||
var count int64
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
||||
import { LanguageSwitcher } from '../common/LanguageSwitcher'
|
||||
import { OnboardingModeSelector } from './OnboardingModeSelector'
|
||||
import type { UserMode } from '../../lib/onboarding'
|
||||
import { invalidateSystemConfig } from '../../lib/config'
|
||||
|
||||
export function LoginPage() {
|
||||
const { language } = useLanguage()
|
||||
@@ -36,6 +37,27 @@ export function LoginPage() {
|
||||
}
|
||||
}, [language])
|
||||
|
||||
const handleResetAccount = async () => {
|
||||
if (!window.confirm(t('forgotAccountConfirm', language))) return
|
||||
try {
|
||||
const res = await fetch('/api/reset-account', { method: 'POST' })
|
||||
if (res.ok) {
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('auth_user')
|
||||
localStorage.removeItem('user_id')
|
||||
sessionStorage.removeItem('from401')
|
||||
invalidateSystemConfig()
|
||||
toast.success(t('forgotAccountSuccess', language))
|
||||
setTimeout(() => { window.location.href = '/setup' }, 1500)
|
||||
} else {
|
||||
const data = await res.json()
|
||||
toast.error(data.error || 'Reset failed')
|
||||
}
|
||||
} catch {
|
||||
toast.error('Network error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
@@ -145,6 +167,16 @@ export function LoginPage() {
|
||||
{loading ? t('loggingIn', language) || 'Signing in...' : t('signIn', language) || 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetAccount}
|
||||
className="text-xs text-zinc-600 hover:text-red-400 transition-colors"
|
||||
>
|
||||
{t('forgotAccount', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -24,8 +24,8 @@ export function OnboardingModeSelector({
|
||||
title: isZh ? '新手模式' : 'Beginner Mode',
|
||||
badge: isZh ? '推荐' : 'Recommended',
|
||||
description: isZh
|
||||
? '自动生成 Base 钱包,默认接入 Claw402 + DeepSeek,最快完成首次启动。'
|
||||
: 'Generate a Base wallet automatically and start with Claw402 + DeepSeek by default.',
|
||||
? '自动生成 Base 钱包,默认接入 Claw402 + GLM,最快完成首次启动。'
|
||||
: 'Generate a Base wallet automatically and start with Claw402 + GLM by default.',
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
|
||||
@@ -536,7 +536,7 @@ function Claw402ConfigForm({
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{CLAW402_MODELS.map((m) => {
|
||||
const isSelected = (modelName || 'deepseek') === m.id
|
||||
const isSelected = (modelName || 'glm-5') === m.id
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
|
||||
@@ -493,6 +493,9 @@ export const translations = {
|
||||
registerNow: 'Sign up now',
|
||||
loginNow: 'Sign in now',
|
||||
forgotPassword: 'Forgot password?',
|
||||
forgotAccount: 'Forgot account?',
|
||||
forgotAccountConfirm: 'This will clear all account data and allow you to register a new account. Continue?',
|
||||
forgotAccountSuccess: 'Account reset successful! You can now register a new account.',
|
||||
rememberMe: 'Remember me',
|
||||
resetPassword: 'Reset Password',
|
||||
resetPasswordTitle: 'Reset your password',
|
||||
@@ -1819,6 +1822,9 @@ export const translations = {
|
||||
registerNow: '立即注册',
|
||||
loginNow: '立即登录',
|
||||
forgotPassword: '忘记密码?',
|
||||
forgotAccount: '忘记账户?',
|
||||
forgotAccountConfirm: '这将清除所有账户数据,允许您重新注册新账户。是否继续?',
|
||||
forgotAccountSuccess: '账户已重置!现在可以注册新账户了。',
|
||||
rememberMe: '记住我',
|
||||
resetPassword: '重置密码',
|
||||
resetPasswordTitle: '重置您的密码',
|
||||
@@ -3080,6 +3086,9 @@ export const translations = {
|
||||
registerNow: 'Daftar sekarang',
|
||||
loginNow: 'Masuk sekarang',
|
||||
forgotPassword: 'Lupa kata sandi?',
|
||||
forgotAccount: 'Lupa akun?',
|
||||
forgotAccountConfirm: 'Ini akan menghapus semua data akun dan memungkinkan Anda mendaftar akun baru. Lanjutkan?',
|
||||
forgotAccountSuccess: 'Akun berhasil direset! Anda sekarang dapat mendaftar akun baru.',
|
||||
rememberMe: 'Ingat saya',
|
||||
resetPassword: 'Reset Kata Sandi',
|
||||
resetPasswordTitle: 'Reset kata sandi Anda',
|
||||
|
||||
Reference in New Issue
Block a user