- 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
249 lines
8.2 KiB
Go
249 lines
8.2 KiB
Go
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
|
||
}
|