mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +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) {
|
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)
|
models, err := s.store.AIModel().List(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", false, err
|
return "", "", "", false, err
|
||||||
@@ -175,6 +176,25 @@ func (s *Server) resolveBeginnerWallet(userID string) (privateKey string, addres
|
|||||||
return existingKey, addr, model.ID, true, nil
|
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()
|
privateKeyObj, genErr := gethcrypto.GenerateKey()
|
||||||
if genErr != nil {
|
if genErr != nil {
|
||||||
return "", "", "", false, genErr
|
return "", "", "", false, genErr
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ func (s *Server) handleRegister(c *gin.Context) {
|
|||||||
return
|
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
|
// Generate JWT token
|
||||||
token, err := auth.GenerateJWT(user.ID, user.Email)
|
token, err := auth.GenerateJWT(user.ID, user.Email)
|
||||||
if err != nil {
|
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"})
|
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
|
// initUserDefaultConfigs Initialize default configs for new user
|
||||||
func (s *Server) initUserDefaultConfigs(userID string, lang string) error {
|
func (s *Server) initUserDefaultConfigs(userID string, lang string) error {
|
||||||
if err := s.createDefaultStrategies(userID, lang); err != nil {
|
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", "/register", "Register new user", s.handleRegister)
|
||||||
s.route(api, "POST", "/login", "User login, returns JWT token", s.handleLogin)
|
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-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
|
// Routes requiring authentication
|
||||||
protected := api.Group("/", s.authMiddleware())
|
protected := api.Group("/", s.authMiddleware())
|
||||||
|
|||||||
@@ -54,6 +54,24 @@ func (s *AIModelStore) initDefaultData() error {
|
|||||||
return nil
|
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
|
// List retrieves user's AI model list
|
||||||
func (s *AIModelStore) List(userID string) ([]*AIModel, error) {
|
func (s *AIModelStore) List(userID string) ([]*AIModel, error) {
|
||||||
var models []*AIModel
|
var models []*AIModel
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ func (s *UserStore) UpdatePassword(userID, passwordHash string) error {
|
|||||||
}).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
|
// EnsureAdmin ensures admin user exists
|
||||||
func (s *UserStore) EnsureAdmin() error {
|
func (s *UserStore) EnsureAdmin() error {
|
||||||
var count int64
|
var count int64
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
|||||||
import { LanguageSwitcher } from '../common/LanguageSwitcher'
|
import { LanguageSwitcher } from '../common/LanguageSwitcher'
|
||||||
import { OnboardingModeSelector } from './OnboardingModeSelector'
|
import { OnboardingModeSelector } from './OnboardingModeSelector'
|
||||||
import type { UserMode } from '../../lib/onboarding'
|
import type { UserMode } from '../../lib/onboarding'
|
||||||
|
import { invalidateSystemConfig } from '../../lib/config'
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const { language } = useLanguage()
|
const { language } = useLanguage()
|
||||||
@@ -36,6 +37,27 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
}, [language])
|
}, [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) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setError('')
|
setError('')
|
||||||
@@ -145,6 +167,16 @@ export function LoginPage() {
|
|||||||
{loading ? t('loggingIn', language) || 'Signing in...' : t('signIn', language) || 'Sign In'}
|
{loading ? t('loggingIn', language) || 'Signing in...' : t('signIn', language) || 'Sign In'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export function OnboardingModeSelector({
|
|||||||
title: isZh ? '新手模式' : 'Beginner Mode',
|
title: isZh ? '新手模式' : 'Beginner Mode',
|
||||||
badge: isZh ? '推荐' : 'Recommended',
|
badge: isZh ? '推荐' : 'Recommended',
|
||||||
description: isZh
|
description: isZh
|
||||||
? '自动生成 Base 钱包,默认接入 Claw402 + DeepSeek,最快完成首次启动。'
|
? '自动生成 Base 钱包,默认接入 Claw402 + GLM,最快完成首次启动。'
|
||||||
: 'Generate a Base wallet automatically and start with Claw402 + DeepSeek by default.',
|
: 'Generate a Base wallet automatically and start with Claw402 + GLM by default.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'advanced',
|
id: 'advanced',
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ function Claw402ConfigForm({
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
{CLAW402_MODELS.map((m) => {
|
{CLAW402_MODELS.map((m) => {
|
||||||
const isSelected = (modelName || 'deepseek') === m.id
|
const isSelected = (modelName || 'glm-5') === m.id
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={m.id}
|
key={m.id}
|
||||||
|
|||||||
@@ -493,6 +493,9 @@ export const translations = {
|
|||||||
registerNow: 'Sign up now',
|
registerNow: 'Sign up now',
|
||||||
loginNow: 'Sign in now',
|
loginNow: 'Sign in now',
|
||||||
forgotPassword: 'Forgot password?',
|
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',
|
rememberMe: 'Remember me',
|
||||||
resetPassword: 'Reset Password',
|
resetPassword: 'Reset Password',
|
||||||
resetPasswordTitle: 'Reset your password',
|
resetPasswordTitle: 'Reset your password',
|
||||||
@@ -1819,6 +1822,9 @@ export const translations = {
|
|||||||
registerNow: '立即注册',
|
registerNow: '立即注册',
|
||||||
loginNow: '立即登录',
|
loginNow: '立即登录',
|
||||||
forgotPassword: '忘记密码?',
|
forgotPassword: '忘记密码?',
|
||||||
|
forgotAccount: '忘记账户?',
|
||||||
|
forgotAccountConfirm: '这将清除所有账户数据,允许您重新注册新账户。是否继续?',
|
||||||
|
forgotAccountSuccess: '账户已重置!现在可以注册新账户了。',
|
||||||
rememberMe: '记住我',
|
rememberMe: '记住我',
|
||||||
resetPassword: '重置密码',
|
resetPassword: '重置密码',
|
||||||
resetPasswordTitle: '重置您的密码',
|
resetPasswordTitle: '重置您的密码',
|
||||||
@@ -3080,6 +3086,9 @@ export const translations = {
|
|||||||
registerNow: 'Daftar sekarang',
|
registerNow: 'Daftar sekarang',
|
||||||
loginNow: 'Masuk sekarang',
|
loginNow: 'Masuk sekarang',
|
||||||
forgotPassword: 'Lupa kata sandi?',
|
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',
|
rememberMe: 'Ingat saya',
|
||||||
resetPassword: 'Reset Kata Sandi',
|
resetPassword: 'Reset Kata Sandi',
|
||||||
resetPasswordTitle: 'Reset kata sandi Anda',
|
resetPasswordTitle: 'Reset kata sandi Anda',
|
||||||
|
|||||||
Reference in New Issue
Block a user