mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-07 03:07:56 +08:00
8e294a5eed
- Delete llm/ dead code (3 files, zero references) - Split mcp/ into sub-packages: mcp/provider/ (8 providers) and mcp/payment/ (4 payment clients) with registry pattern - Export Client internal fields and ClientHooks interface for sub-package access - Split api/server.go (3892 lines) into 8 domain-specific handler files - Split trader/auto_trader.go (2296 lines) into 5 focused files - Reorganize web/src/components/ flat files into auth/, charts/, trader/, common/, modals/, backtest/ subdirectories - Update all consumer imports to use registry-based provider creation
224 lines
6.1 KiB
Go
224 lines
6.1 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"nofx/auth"
|
|
"nofx/logger"
|
|
"nofx/store"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// handleLogout Add current token to blacklist
|
|
func (s *Server) handleLogout(c *gin.Context) {
|
|
authHeader := c.GetHeader("Authorization")
|
|
if authHeader == "" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"})
|
|
return
|
|
}
|
|
parts := strings.Split(authHeader, " ")
|
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
|
|
return
|
|
}
|
|
tokenString := parts[1]
|
|
claims, err := auth.ValidateJWT(tokenString)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
|
return
|
|
}
|
|
var exp time.Time
|
|
if claims.ExpiresAt != nil {
|
|
exp = claims.ExpiresAt.Time
|
|
} else {
|
|
exp = time.Now().Add(24 * time.Hour)
|
|
}
|
|
auth.BlacklistToken(tokenString, exp)
|
|
c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
|
|
}
|
|
|
|
// handleRegister Handle user registration request.
|
|
// handleRegister allows registration only when no users exist yet (first-time setup).
|
|
// This is a single-user system; subsequent registrations are permanently closed.
|
|
func (s *Server) handleRegister(c *gin.Context) {
|
|
userCount, err := s.store.User().Count()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check user count"})
|
|
return
|
|
}
|
|
|
|
if userCount > 0 {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "System already initialized"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
Password string `json:"password" binding:"required,min=6"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
SafeBadRequest(c, "Invalid request parameters")
|
|
return
|
|
}
|
|
|
|
// Check if email already exists
|
|
_, err = s.store.User().GetByEmail(req.Email)
|
|
if err == nil {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "Email already registered"})
|
|
return
|
|
}
|
|
|
|
// Generate password hash
|
|
passwordHash, err := auth.HashPassword(req.Password)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Password processing failed"})
|
|
return
|
|
}
|
|
|
|
// Create user
|
|
userID := uuid.New().String()
|
|
user := &store.User{
|
|
ID: userID,
|
|
Email: req.Email,
|
|
PasswordHash: passwordHash,
|
|
}
|
|
|
|
err = s.store.User().Create(user)
|
|
if err != nil {
|
|
SafeInternalError(c, "Failed to create user", err)
|
|
return
|
|
}
|
|
|
|
// Generate JWT token
|
|
token, err := auth.GenerateJWT(user.ID, user.Email)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
|
return
|
|
}
|
|
|
|
// Initialize default model and exchange configs for user
|
|
err = s.initUserDefaultConfigs(user.ID)
|
|
if err != nil {
|
|
logger.Infof("Failed to initialize user default configs: %v", err)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"token": token,
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
"message": "Registration successful",
|
|
})
|
|
}
|
|
|
|
// handleLogin Handle user login request
|
|
func (s *Server) handleLogin(c *gin.Context) {
|
|
var req struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
Password string `json:"password" binding:"required"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
SafeBadRequest(c, "Invalid request parameters")
|
|
return
|
|
}
|
|
|
|
// Get user information
|
|
user, err := s.store.User().GetByEmail(req.Email)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Email or password incorrect"})
|
|
return
|
|
}
|
|
|
|
// Verify password
|
|
if !auth.CheckPassword(req.Password, user.PasswordHash) {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Email or password incorrect"})
|
|
return
|
|
}
|
|
|
|
// Issue token directly after password verification.
|
|
token, err := auth.GenerateJWT(user.ID, user.Email)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"token": token,
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
"message": "Login successful",
|
|
})
|
|
}
|
|
|
|
// handleChangePassword changes the password for the currently authenticated user.
|
|
func (s *Server) handleChangePassword(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
var req struct {
|
|
NewPassword string `json:"new_password" binding:"required,min=8"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
SafeBadRequest(c, "new_password is required (min 8 chars)")
|
|
return
|
|
}
|
|
hash, err := auth.HashPassword(req.NewPassword)
|
|
if err != nil {
|
|
SafeInternalError(c, "Password processing failed", err)
|
|
return
|
|
}
|
|
if err := s.store.User().UpdatePassword(userID, hash); err != nil {
|
|
SafeInternalError(c, "Failed to update password", err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "Password updated"})
|
|
}
|
|
|
|
// handleResetPassword Reset password via email and new password
|
|
func (s *Server) handleResetPassword(c *gin.Context) {
|
|
var req struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
NewPassword string `json:"new_password" binding:"required,min=6"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
SafeBadRequest(c, "Invalid request parameters")
|
|
return
|
|
}
|
|
|
|
// Query user
|
|
user, err := s.store.User().GetByEmail(req.Email)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Email does not exist"})
|
|
return
|
|
}
|
|
|
|
// Generate new password hash
|
|
newPasswordHash, err := auth.HashPassword(req.NewPassword)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Password processing failed"})
|
|
return
|
|
}
|
|
|
|
// Update password
|
|
err = s.store.User().UpdatePassword(user.ID, newPasswordHash)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Password update failed"})
|
|
return
|
|
}
|
|
|
|
logger.Infof("✓ User %s password has been reset", user.Email)
|
|
c.JSON(http.StatusOK, gin.H{"message": "Password reset successful, please login with new password"})
|
|
}
|
|
|
|
// initUserDefaultConfigs Initialize default model and exchange configs for new user
|
|
func (s *Server) initUserDefaultConfigs(userID string) error {
|
|
// Commented out auto-creation of default configs, let users add manually
|
|
// This way new users won't have config items automatically after registration
|
|
logger.Infof("User %s registration completed, waiting for manual AI model and exchange configuration", userID)
|
|
return nil
|
|
}
|