Files
91-api-server/internal/handler/settings_handler.go
yuqianhe 3e27017e42 feat: add auth, db, gateway, email, ratelimit, geoblock modules and new handlers
- New CLI commands: key, user
- New internal modules: auth (middleware, password, user), db (db, logs, products), email, gateway (gateway, hooks, middlewares), handler (admin, auth, dev, log, product, settings), middleware/geoblock, ratelimit
- New demo frontend page
- Updated config, server, analytics, stats, and existing frontend pages
2026-06-26 21:32:51 +09:00

249 lines
8.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"time"
"api-server/internal/auth"
"api-server/internal/db"
"api-server/internal/email"
"github.com/gin-gonic/gin"
)
type SettingsHandler struct {
db *db.DB
smtp *email.Sender
}
func NewSettingsHandler(database *db.DB, smtp *email.Sender) *SettingsHandler {
return &SettingsHandler{db: database, smtp: smtp}
}
func (h *SettingsHandler) RegisterRoutes(r *gin.RouterGroup) {
// 管理员:系统设置
admin := r.Group("/admin")
admin.Use(auth.RequireAuth(h.db), auth.RequireAdmin())
admin.GET("/settings", h.GetSettings)
admin.PUT("/settings", h.UpdateSettings)
admin.POST("/settings/test-email", h.TestEmail)
// 公开:忘记密码 / 重置密码 / 验证邮箱
r.POST("/auth/forgot-password", h.ForgotPassword)
r.POST("/auth/reset-password", h.ResetPassword)
r.POST("/auth/verify-email", h.VerifyEmail)
// 登录用户:发送验证邮件 / 修改邮箱
authed := r.Group("")
authed.Use(auth.RequireAuth(h.db))
authed.POST("/auth/send-verification", h.SendVerification)
authed.PUT("/auth/email", h.UpdateEmail)
}
func (h *SettingsHandler) GetSettings(c *gin.Context) {
cfg, err := h.db.GetSystemSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": cfg})
}
func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
var req map[string]string
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "参数错误"})
return
}
for k, v := range req {
if err := h.db.SetSystemSetting(k, v); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": fmt.Sprintf("设置 %s 失败: %s", k, err.Error())})
return
}
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "已保存"})
}
func (h *SettingsHandler) TestEmail(c *gin.Context) {
var req struct {
To string `json:"to" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请提供收件地址"})
return
}
if !h.smtp.Enabled() {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "SMTP 未配置"})
return
}
if err := h.smtp.Send(req.To, "测试邮件", "<h1>测试</h1><p>这是一封测试邮件SMTP 配置正常。</p>"); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": fmt.Sprintf("发送失败: %s", err.Error())})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "发送成功"})
}
func (h *SettingsHandler) ForgotPassword(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请提供邮箱"})
return
}
user, err := h.db.GetUserByEmail(req.Email)
if err != nil {
// 不暴露邮箱是否存在
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "如果该邮箱已注册,将收到重置邮件"})
return
}
if !h.smtp.Enabled() {
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "如果该邮箱已注册,将收到重置邮件"})
return
}
token, err := generateToken()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "生成令牌失败"})
return
}
if err := h.db.CreateResetToken(user.ID, token, time.Now().Add(1*time.Hour)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "创建令牌失败"})
return
}
cfg, _ := h.db.GetSystemSettings()
siteName := cfg["site_name"]
if siteName == "" {
siteName = "API Platform"
}
body := fmt.Sprintf(`<h2>密码重置</h2><p>你请求重置 %s 的密码:</p><p><a href="%s">点击此处重置密码</a></p><p>或复制以下链接:<br>%s</p><p>链接有效期为 1 小时。</p>`,
siteName, token, token)
h.smtp.Send(req.Email, fmt.Sprintf("[%s] 密码重置", siteName), body)
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "如果该邮箱已注册,将收到重置邮件"})
}
func (h *SettingsHandler) ResetPassword(c *gin.Context) {
var req struct {
Token string `json:"token" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请提供令牌和新密码至少6位"})
return
}
userID, err := h.db.GetResetTokenByToken(req.Token)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "令牌无效或已过期"})
return
}
hash, err := auth.HashPassword(req.NewPassword)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "密码加密失败"})
return
}
user, err := h.db.GetUserByID(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "用户不存在"})
return
}
if err := h.db.UpdatePassword(user.Email, hash); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": err.Error()})
return
}
h.db.DeleteResetToken(req.Token)
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码已重置,请重新登录"})
}
func (h *SettingsHandler) SendVerification(c *gin.Context) {
user := auth.GetUser(c)
if !h.smtp.Enabled() {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "邮件服务未配置"})
return
}
token, err := generateToken()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "生成令牌失败"})
return
}
if err := h.db.CreateVerificationToken(user.ID, user.Email, token, time.Now().Add(1*time.Hour)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": "创建令牌失败"})
return
}
cfg, _ := h.db.GetSystemSettings()
siteName := cfg["site_name"]
if siteName == "" {
siteName = "API Platform"
}
body := fmt.Sprintf(`<h2>验证邮箱</h2><p>请点击以下链接验证你的邮箱:</p><p><a href="%s">点击验证</a></p><p>令牌:%s</p><p>链接有效期为 1 小时。</p>`,
token, token)
h.smtp.Send(user.Email, fmt.Sprintf("[%s] 邮箱验证", siteName), body)
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "验证邮件已发送"})
}
func (h *SettingsHandler) VerifyEmail(c *gin.Context) {
var req struct {
Token string `json:"token" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请提供验证令牌"})
return
}
userID, email, err := h.db.GetVerificationToken(req.Token)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "令牌无效或已过期"})
return
}
// 更新邮箱已验证状态
if err := h.db.UpdateEmailVerified(userID, true); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": err.Error()})
return
}
h.db.DeleteVerificationToken(req.Token)
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "邮箱已验证", "data": gin.H{"email": email}})
}
func (h *SettingsHandler) UpdateEmail(c *gin.Context) {
user := auth.GetUser(c)
var req struct {
Email string `json:"email" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请提供新邮箱"})
return
}
if err := h.db.UpdateUserEmail(user.ID, req.Email); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": err.Error()})
return
}
// 如果有 SMTP 配置,发送验证邮件
if h.smtp.Enabled() {
token, err := generateToken()
if err == nil {
h.db.CreateVerificationToken(user.ID, req.Email, token, time.Now().Add(1*time.Hour))
cfg, _ := h.db.GetSystemSettings()
siteName := cfg["site_name"]
if siteName == "" {
siteName = "API Platform"
}
body := fmt.Sprintf(`<h2>验证新邮箱</h2><p>请点击以下链接验证你的新邮箱:</p><p><a href="%s">点击验证</a></p>`, token)
h.smtp.Send(req.Email, fmt.Sprintf("[%s] 邮箱验证", siteName), body)
}
}
updated, err := h.db.GetUserByID(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 1, "msg": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "邮箱已修改", "data": userResponse(updated)})
}
func generateToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}