diff --git a/api/core/app_server.go b/api/core/app_server.go index c166c0dc..809b8a7b 100644 --- a/api/core/app_server.go +++ b/api/core/app_server.go @@ -146,6 +146,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc { if c.Request.URL.Path == "/api/user/login" || c.Request.URL.Path == "/api/user/resetPass" || c.Request.URL.Path == "/api/admin/login" || + c.Request.URL.Path == "/api/admin/login/captcha" || c.Request.URL.Path == "/api/user/register" || c.Request.URL.Path == "/api/chat/history" || c.Request.URL.Path == "/api/chat/detail" || diff --git a/api/core/types/config.go b/api/core/types/config.go index bb71dd8d..2acf2856 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -124,8 +124,10 @@ func (c RedisConfig) Url() string { // Manager 管理员 type Manager struct { - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username"` + Password string `json:"password"` + Captcha string `json:"captcha"` // 验证码 + CaptchaId string `json:"captcha_id"` // 验证码id } // ChatConfig 系统默认的聊天配置 diff --git a/api/go.mod b/api/go.mod index aa79bd42..30947c4b 100644 --- a/api/go.mod +++ b/api/go.mod @@ -27,7 +27,15 @@ require github.com/xxl-job/xxl-job-executor-go v1.2.0 require github.com/bg5t/mydiscordgo v0.28.1 -require github.com/shopspring/decimal v1.3.1 // indirect +require ( + github.com/mojocn/base64Captcha v1.3.1 + github.com/shopspring/decimal v1.3.1 +) + +require ( + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect +) require ( github.com/andybalholm/brotli v1.0.4 // indirect diff --git a/api/go.sum b/api/go.sum index 44f4fca5..ad58bfcd 100644 --- a/api/go.sum +++ b/api/go.sum @@ -65,6 +65,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -129,6 +131,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mojocn/base64Captcha v1.3.1 h1:2Wbkt8Oc8qjmNJ5GyOfSo4tgVQPsbKMftqASnq8GlT0= +github.com/mojocn/base64Captcha v1.3.1/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -229,6 +233,8 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 h1:TbGuee8sSq15Iguxu4deQ7+Bqq/d2rsQejGcEtADAMQ= +golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= diff --git a/api/handler/admin/admin_handler.go b/api/handler/admin/admin_handler.go index dbb2611d..c11dba65 100644 --- a/api/handler/admin/admin_handler.go +++ b/api/handler/admin/admin_handler.go @@ -9,6 +9,7 @@ import ( "context" "github.com/go-redis/redis/v8" "github.com/golang-jwt/jwt/v5" + "github.com/mojocn/base64Captcha" "time" "github.com/gin-gonic/gin" @@ -36,6 +37,13 @@ func (h *ManagerHandler) Login(c *gin.Context) { resp.ERROR(c, types.InvalidArgs) return } + + // add captcha + if !base64Captcha.DefaultMemStore.Verify(data.CaptchaId, data.Captcha, true) { + resp.ERROR(c, "验证码错误,请重新输入!") + return + } + manager := h.App.Config.Manager if data.Username == manager.Username && data.Password == manager.Password { // 创建 token diff --git a/api/handler/admin/admin_user_handler.go b/api/handler/admin/admin_user_handler.go index 99c49f79..20312ed5 100644 --- a/api/handler/admin/admin_user_handler.go +++ b/api/handler/admin/admin_user_handler.go @@ -64,11 +64,10 @@ func (h *SysUserHandler) List(c *gin.Context) { // Save 更新或者新增 func (h *SysUserHandler) Save(c *gin.Context) { var data struct { - Id uint `json:"id"` - Password string `json:"password"` - Username string `json:"username"` - ExpiredTime string `json:"expired_time"` - Status bool `json:"status"` + Id uint `json:"id"` + Password string `json:"password"` + Username string `json:"username"` + Status bool `json:"status"` } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) @@ -81,18 +80,16 @@ func (h *SysUserHandler) Save(c *gin.Context) { user.Id = data.Id // 此处需要用 map 更新,用结构体无法更新 0 值 res = h.db.Model(&user).Updates(map[string]interface{}{ - "username": data.Username, - "status": data.Status, - "expired_time": utils.Str2stamp(data.ExpiredTime), + "username": data.Username, + "status": data.Status, }) } else { salt := utils.RandString(8) u := model.AdminUser{ - Username: data.Username, - Password: utils.GenPassword(data.Password, salt), - Salt: salt, - Status: true, - ExpiredTime: utils.Str2stamp(data.ExpiredTime), + Username: data.Username, + Password: utils.GenPassword(data.Password, salt), + Salt: salt, + Status: true, } res = h.db.Create(&u) _ = utils.CopyObject(u, &userVo) diff --git a/api/handler/admin/captcha_handler.go b/api/handler/admin/captcha_handler.go new file mode 100644 index 00000000..a056b3c4 --- /dev/null +++ b/api/handler/admin/captcha_handler.go @@ -0,0 +1,41 @@ +package admin + +import ( + "chatplus/core" + "chatplus/handler" + "chatplus/utils/resp" + "github.com/gin-gonic/gin" + "github.com/mojocn/base64Captcha" +) + +type CaptchaHandler struct { + handler.BaseHandler +} + +func NewCaptchaHandler(app *core.AppServer) *CaptchaHandler { + h := CaptchaHandler{} + h.App = app + return &h +} + +type CaptchaVo struct { + CaptchaId string `json:"captcha_id"` + PicPath string `json:"pic_path"` +} + +// GetCaptcha 获取验证码 +func (h *CaptchaHandler) GetCaptcha(c *gin.Context) { + var captchaVo CaptchaVo + driver := base64Captcha.NewDriverDigit(48, 130, 4, 0.4, 10) + cp := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore) + // b64s是图片的base64编码 + id, b64s, err := cp.Generate() + if err != nil { + resp.ERROR(c, "生成验证码错误!") + return + } + captchaVo.CaptchaId = id + captchaVo.PicPath = b64s + + resp.SUCCESS(c, captchaVo) +} diff --git a/api/handler/admin/upload_handler.go b/api/handler/admin/upload_handler.go new file mode 100644 index 00000000..2b0a9ce8 --- /dev/null +++ b/api/handler/admin/upload_handler.go @@ -0,0 +1,97 @@ +package admin + +import ( + "chatplus/core" + "chatplus/handler" + "chatplus/service/oss" + "chatplus/store/model" + "chatplus/store/vo" + "chatplus/utils" + "chatplus/utils/resp" + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "time" +) + +type UploadHandler struct { + handler.BaseHandler + db *gorm.DB + uploaderManager *oss.UploaderManager +} + +func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler { + adminHandler := &UploadHandler{db: db, uploaderManager: manager} + adminHandler.App = app + return adminHandler +} + +func (h *UploadHandler) Upload(c *gin.Context) { + file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file") + if err != nil { + resp.ERROR(c, err.Error()) + return + } + userId := 0 + res := h.db.Create(&model.File{ + UserId: userId, + Name: file.Name, + ObjKey: file.ObjKey, + URL: file.URL, + Ext: file.Ext, + Size: file.Size, + CreatedAt: time.Time{}, + }) + if res.Error != nil || res.RowsAffected == 0 { + resp.ERROR(c, "error with update database: "+res.Error.Error()) + return + } + + resp.SUCCESS(c, file) +} + +func (h *UploadHandler) List(c *gin.Context) { + userId := 0 + var items []model.File + var files = make([]vo.File, 0) + h.db.Where("user_id = ?", userId).Find(&items) + if len(items) > 0 { + for _, v := range items { + var file vo.File + err := utils.CopyObject(v, &file) + if err != nil { + logger.Error(err) + continue + } + file.CreatedAt = v.CreatedAt.Unix() + files = append(files, file) + } + } + + resp.SUCCESS(c, files) +} + +// Remove remove files +func (h *UploadHandler) Remove(c *gin.Context) { + userId := 0 + id := h.GetInt(c, "id", 0) + var file model.File + tx := h.db.Where("user_id = ? AND id = ?", userId, id).First(&file) + if tx.Error != nil || file.Id == 0 { + resp.ERROR(c, "file not existed") + return + } + + // remove database + tx = h.db.Model(&model.File{}).Delete("id = ?", id) + if tx.Error != nil || tx.RowsAffected == 0 { + resp.ERROR(c, "failed to update database") + return + } + // remove files + objectKey := file.ObjKey + if objectKey == "" { + objectKey = file.URL + } + _ = h.uploaderManager.GetUploadHandler().Delete(objectKey) + resp.SUCCESS(c) +} diff --git a/api/handler/upload_handler.go b/api/handler/upload_handler.go index 2a85b5df..4bc81b43 100644 --- a/api/handler/upload_handler.go +++ b/api/handler/upload_handler.go @@ -33,7 +33,7 @@ func (h *UploadHandler) Upload(c *gin.Context) { userId := h.GetLoginUserId(c) res := h.db.Create(&model.File{ - UserId: userId, + UserId: int(userId), Name: file.Name, ObjKey: file.ObjKey, URL: file.URL, diff --git a/api/main.go b/api/main.go index 2a44ea99..3fac07ad 100644 --- a/api/main.go +++ b/api/main.go @@ -369,16 +369,30 @@ func main() { group.GET("token", h.GenToken) }), - // 系统管理员 - fx.Provide(admin.NewSysUserHandler), - fx.Invoke(func(s *core.AppServer, h *admin.SysUserHandler) { - group := s.Engine.Group("/api/admin/sysUser/") - group.POST("save", h.Save) - group.GET("list", h.List) - group.POST("remove", h.Remove) - group.POST("resetPass", h.ResetPass) + // 验证码 + fx.Provide(admin.NewCaptchaHandler), + fx.Invoke(func(s *core.AppServer, h *admin.CaptchaHandler) { + group := s.Engine.Group("/api/admin/login/") + group.GET("captcha", h.GetCaptcha) }), + fx.Provide(admin.NewUploadHandler), + fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) { + s.Engine.POST("/api/admin/upload", h.Upload) + s.Engine.GET("/api/admin/upload/list", h.List) + s.Engine.GET("/api/admin/upload/remove", h.Remove) + }), + + //// 系统管理员 + //fx.Provide(admin.NewSysUserHandler), + //fx.Invoke(func(s *core.AppServer, h *admin.SysUserHandler) { + // group := s.Engine.Group("/api/admin/sysUser/") + // group.POST("save", h.Save) + // group.GET("list", h.List) + // group.POST("remove", h.Remove) + // group.POST("resetPass", h.ResetPass) + //}), + fx.Provide(handler.NewFunctionHandler), fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) { group := s.Engine.Group("/api/function/") diff --git a/api/store/model/admin_user.go b/api/store/model/admin_user.go index 1c694741..ffeccd17 100644 --- a/api/store/model/admin_user.go +++ b/api/store/model/admin_user.go @@ -5,7 +5,6 @@ type AdminUser struct { Username string Password string Salt string // 密码盐 - ExpiredTime int64 // 账户到期时间 Status bool `gorm:"default:true"` // 当前状态 LastLoginAt int64 // 最后登录时间 LastLoginIp string // 最后登录 IP diff --git a/api/store/model/file.go b/api/store/model/file.go index 7541ec8d..56fe424d 100644 --- a/api/store/model/file.go +++ b/api/store/model/file.go @@ -4,7 +4,7 @@ import "time" type File struct { Id uint `gorm:"primarykey;column:id"` - UserId uint + UserId int Name string ObjKey string URL string diff --git a/api/store/vo/admin_user.go b/api/store/vo/admin_user.go index e2349678..92512461 100644 --- a/api/store/vo/admin_user.go +++ b/api/store/vo/admin_user.go @@ -4,7 +4,6 @@ type AdminUser struct { BaseVo Username string `json:"username"` Salt string `json:"salt"` // 密码盐 - ExpiredTime int64 `json:"expired_time"` // 账户到期时间 Status bool `json:"status"` // 当前状态 LastLoginAt int64 `json:"last_login_at"` // 最后登录时间 LastLoginIp string `json:"last_login_ip"` // 最后登录 IP