package handlers import ( "database/sql" "net/http" "strings" "time" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" ) type AuthHandler struct { db *sql.DB } func NewAuthHandler(db *sql.DB) *AuthHandler { return &AuthHandler{db: db} } // Login renders the login page. func (h *AuthHandler) Login(c *gin.Context) { c.HTML(http.StatusOK, "pages/login.html", gin.H{}) } // Register renders the registration page. func (h *AuthHandler) Register(c *gin.Context) { c.HTML(http.StatusOK, "pages/register.html", gin.H{}) } // HandleLogin processes POST /api/auth/login. func (h *AuthHandler) HandleLogin(c *gin.Context) { var req struct { Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱和密码为必填项"}) return } req.Email = strings.TrimSpace(strings.ToLower(req.Email)) var user struct { ID int64 PasswordHash string } err := h.db.QueryRow(`SELECT id, password_hash FROM users WHERE email = ?`, req.Email). Scan(&user.ID, &user.PasswordHash) if err == sql.ErrNoRows { c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器错误"}) return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) return } // Set session session := sessions.Default(c) session.Set("user_id", user.ID) if err := session.Save(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "会话保存失败"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // HandleRegister processes POST /api/auth/register. func (h *AuthHandler) HandleRegister(c *gin.Context) { var req struct { Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱和密码为必填项"}) return } req.Email = strings.TrimSpace(strings.ToLower(req.Email)) // Validate email format (basic check) if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") { c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"}) return } // Validate password length if len(req.Password) < 6 { c.JSON(http.StatusBadRequest, gin.H{"error": "密码长度至少为 6 位"}) return } // Check if email already exists var exists int h.db.QueryRow(`SELECT COUNT(*) FROM users WHERE email = ?`, req.Email).Scan(&exists) if exists > 0 { c.JSON(http.StatusConflict, gin.H{"error": "该邮箱已被注册"}) return } // Hash password hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"}) return } // Insert user now := time.Now().UTC() result, err := h.db.Exec(`INSERT INTO users (email, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?)`, req.Email, string(hash), now, now) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"}) return } userID, _ := result.LastInsertId() // Seed default settings for the user for key, val := range defaultUserSettings { h.db.Exec(`INSERT OR IGNORE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)`, userID, key, val) } // Auto-login: set session session := sessions.Default(c) session.Set("user_id", userID) if err := session.Save(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "会话保存失败"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // HandleLogout processes POST /api/auth/logout. func (h *AuthHandler) HandleLogout(c *gin.Context) { session := sessions.Default(c) session.Clear() session.Save() c.Redirect(http.StatusFound, "/login") } // defaultUserSettings mirrors models.DefaultSettings for seeding new users. var defaultUserSettings = map[string]string{ "llm.endpoint": "https://api.deepseek.com", "llm.api_key": "", "llm.model": "deepseek-v4-pro", "review.top_n": "20", "review.concurrency": "5", "cache.max_age_days": "7", "cache.max_size_mb": "5000", }