From 59132669691cf9e6ed6b1b1f1ff68163502a1687 Mon Sep 17 00:00:00 2001 From: JustSong Date: Tue, 22 Nov 2022 11:04:06 +0800 Subject: [PATCH] chore: sync with gin-template --- common/constants.go | 5 + common/email.go | 4 +- common/embed-file-system.go | 2 +- common/init.go | 6 +- common/verification.go | 2 +- controller/file.go | 131 -------------- controller/github.go | 22 ++- controller/misc.go | 2 + controller/option.go | 6 + controller/user.go | 40 +++-- controller/wechat.go | 20 ++- go.mod | 7 +- go.sum | 32 +++- main.go | 12 +- middleware/auth.go | 69 ++++++- middleware/cache.go | 12 ++ middleware/cors.go | 12 +- middleware/turnstile-check.go | 80 +++++++++ model/file.go | 53 ------ model/main.go | 7 +- model/option.go | 38 ++-- model/user.go | 30 +++- router/api-router.go | 18 +- router/web-router.go | 3 +- web/package.json | 1 + web/src/App.js | 48 +++-- web/src/components/Footer.js | 14 +- web/src/components/Header.js | 41 +++-- web/src/components/LoginForm.js | 4 +- web/src/components/OtherSetting.js | 21 ++- web/src/components/PasswordResetConfirm.js | 4 +- web/src/components/PasswordResetForm.js | 39 +++- web/src/components/PersonalSetting.js | 69 ++++++- web/src/components/RegisterForm.js | 53 +++++- web/src/components/SystemSetting.js | 199 ++++++++++++++++----- web/src/components/UsersTable.js | 106 +++++++---- web/src/constants/common.constant.js | 1 + web/src/constants/index.js | 3 +- web/src/helpers/utils.js | 4 + web/src/pages/About/index.js | 14 +- web/src/pages/User/EditUser.js | 69 +++---- 41 files changed, 813 insertions(+), 490 deletions(-) delete mode 100644 controller/file.go create mode 100644 middleware/cache.go create mode 100644 middleware/turnstile-check.go delete mode 100644 model/file.go create mode 100644 web/src/constants/common.constant.js diff --git a/common/constants.go b/common/constants.go index 4520b91..a195081 100644 --- a/common/constants.go +++ b/common/constants.go @@ -27,6 +27,8 @@ var PasswordRegisterEnabled = true var EmailVerificationEnabled = false var GitHubOAuthEnabled = false var WeChatAuthEnabled = false +var TurnstileCheckEnabled = false +var RegisterEnabled = true var SMTPServer = "" var SMTPAccount = "" @@ -39,6 +41,9 @@ var WeChatServerAddress = "" var WeChatServerToken = "" var WeChatAccountQRCodeImageURL = "" +var TurnstileSiteKey = "" +var TurnstileSecretKey = "" + const ( RoleGuestUser = 0 RoleCommonUser = 1 diff --git a/common/email.go b/common/email.go index 4b65eb9..8e293be 100644 --- a/common/email.go +++ b/common/email.go @@ -1,8 +1,6 @@ package common -import ( - "gopkg.in/gomail.v2" -) +import "gopkg.in/gomail.v2" func SendEmail(subject string, receiver string, content string) error { m := gomail.NewMessage() diff --git a/common/embed-file-system.go b/common/embed-file-system.go index 3ea84ec..3ea02cf 100644 --- a/common/embed-file-system.go +++ b/common/embed-file-system.go @@ -2,7 +2,7 @@ package common import ( "embed" - "github.com/gin-gonic/contrib/static" + "github.com/gin-contrib/static" "io/fs" "net/http" ) diff --git a/common/init.go b/common/init.go index 32e0cf6..66fcd7e 100644 --- a/common/init.go +++ b/common/init.go @@ -12,9 +12,9 @@ var ( Port = flag.Int("port", 3000, "the listening port") PrintVersion = flag.Bool("version", false, "print version and exit") LogDir = flag.String("log-dir", "", "specify the log directory") - //Host = flag.Key("host", "localhost", "the server's ip address or domain") - //Path = flag.Key("path", "", "specify a local path to public") - //VideoPath = flag.Key("video", "", "specify a video folder to public") + //Host = flag.String("host", "localhost", "the server's ip address or domain") + //Path = flag.String("path", "", "specify a local path to public") + //VideoPath = flag.String("video", "", "specify a video folder to public") //NoBrowser = flag.Bool("no-browser", false, "open browser or not") ) diff --git a/common/verification.go b/common/verification.go index cea9bda..d8ccd6e 100644 --- a/common/verification.go +++ b/common/verification.go @@ -60,7 +60,7 @@ func DeleteKey(key string, purpose string) { delete(verificationMap, purpose+key) } -// no lock inside! +// no lock inside, so the caller must lock the verificationMap before calling! func removeExpiredPairs() { now := time.Now() for key := range verificationMap { diff --git a/controller/file.go b/controller/file.go deleted file mode 100644 index d7c1084..0000000 --- a/controller/file.go +++ /dev/null @@ -1,131 +0,0 @@ -package controller - -import ( - "encoding/json" - "fmt" - "github.com/gin-gonic/gin" - "message-pusher/common" - "message-pusher/model" - "net/http" - "os" - "path/filepath" - "strings" - "time" -) - -type FileDeleteRequest struct { - Id int - Link string - //Token string -} - -func UploadFile(c *gin.Context) { - uploadPath := common.UploadPath - //saveToDatabase := true - //path := c.PostForm("path") - //if path != "" { // Upload to explorer's path - // uploadPath = filepath.Join(common.ExplorerRootPath, path) - // if !strings.HasPrefix(uploadPath, common.ExplorerRootPath) { - // // In this case the given path is not valid, so we reset it to ExplorerRootPath. - // uploadPath = common.ExplorerRootPath - // } - // saveToDatabase = false - //} - - description := c.PostForm("description") - if description == "" { - description = "无描述信息" - } - uploader := c.GetString("username") - if uploader == "" { - uploader = "匿名用户" - } - currentTime := time.Now().Format("2006-01-02 15:04:05") - form, err := c.MultipartForm() - if err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error())) - return - } - files := form.File["file"] - for _, file := range files { - // In case someone wants to upload to other folders. - filename := filepath.Base(file.Filename) - link := filename - savePath := filepath.Join(uploadPath, filename) - if _, err := os.Stat(savePath); err == nil { - // File already existed. - t := time.Now() - timestamp := t.Format("_2006-01-02_15-04-05") - ext := filepath.Ext(filename) - if ext == "" { - link += timestamp - } else { - link = filename[:len(filename)-len(ext)] + timestamp + ext - } - savePath = filepath.Join(uploadPath, link) - } - if err := c.SaveUploadedFile(file, savePath); err != nil { - c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error())) - return - } - // save to database - fileObj := &model.File{ - Description: description, - Uploader: uploader, - Time: currentTime, - Link: link, - Filename: filename, - } - err = fileObj.Insert() - if err != nil { - _ = fmt.Errorf(err.Error()) - } - } - c.Redirect(http.StatusSeeOther, "./") -} - -func DeleteFile(c *gin.Context) { - var deleteRequest FileDeleteRequest - err := json.NewDecoder(c.Request.Body).Decode(&deleteRequest) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "message": "无效的参数", - }) - return - } - - fileObj := &model.File{ - Id: deleteRequest.Id, - } - model.DB.Where("id = ?", deleteRequest.Id).First(&fileObj) - err = fileObj.Delete() - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": err.Error(), - }) - } else { - message := "文件删除成功" - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": message, - }) - } - -} - -func DownloadFile(c *gin.Context) { - path := c.Param("file") - fullPath := filepath.Join(common.UploadPath, path) - if !strings.HasPrefix(fullPath, common.UploadPath) { - // We may being attacked! - c.Status(403) - return - } - c.File(fullPath) - // Update download counter - go func() { - model.UpdateDownloadCounter(path) - }() -} diff --git a/controller/github.go b/controller/github.go index 76cf402..cd1aea1 100644 --- a/controller/github.go +++ b/controller/github.go @@ -107,16 +107,24 @@ func GitHubOAuth(c *gin.Context) { if model.IsGitHubIdAlreadyTaken(user.GitHubId) { user.FillUserByGitHubId() } else { - user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1) - user.DisplayName = githubUser.Name - user.Email = githubUser.Email - user.Role = common.RoleCommonUser - user.Status = common.UserStatusEnabled + if common.RegisterEnabled { + user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1) + user.DisplayName = githubUser.Name + user.Email = githubUser.Email + user.Role = common.RoleCommonUser + user.Status = common.UserStatusEnabled - if err := user.Insert(); err != nil { + if err := user.Insert(); err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + } else { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": err.Error(), + "message": "管理员关闭了新用户注册", }) return } diff --git a/controller/misc.go b/controller/misc.go index ffa7f16..6ea1634 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -24,6 +24,8 @@ func GetStatus(c *gin.Context) { "wechat_qrcode": common.WeChatAccountQRCodeImageURL, "wechat_login": common.WeChatAuthEnabled, "server_address": common.ServerAddress, + "turnstile_check": common.TurnstileCheckEnabled, + "turnstile_site_key": common.TurnstileSiteKey, }, }) return diff --git a/controller/option.go b/controller/option.go index a7fa9bf..f5b3bf6 100644 --- a/controller/option.go +++ b/controller/option.go @@ -52,6 +52,12 @@ func UpdateOption(c *gin.Context) { "message": "无法启用微信登录,请先填入微信登录相关配置信息!", }) return + } else if option.Key == "TurnstileCheckEnabled" && option.Value == "true" && common.TurnstileSiteKey == "" { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!", + }) + return } err = model.UpdateOption(option.Key, option.Value) if err != nil { diff --git a/controller/user.go b/controller/user.go index 8947225..f84d00f 100644 --- a/controller/user.go +++ b/controller/user.go @@ -100,13 +100,20 @@ func Logout(c *gin.Context) { } func Register(c *gin.Context) { - if !common.PasswordRegisterEnabled { + if !common.RegisterEnabled { c.JSON(http.StatusOK, gin.H{ "message": "管理员关闭了新用户注册", "success": false, }) return } + if !common.PasswordRegisterEnabled { + c.JSON(http.StatusOK, gin.H{ + "message": "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册", + "success": false, + }) + return + } var user model.User err := json.NewDecoder(c.Request.Body).Decode(&user) if err != nil { @@ -134,7 +141,7 @@ func Register(c *gin.Context) { if !common.VerifyCodeWithKey(user.Email, user.VerificationCode, common.EmailVerificationPurpose) { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "验证码错误!", + "message": "验证码错误或已过期", }) return } @@ -154,7 +161,6 @@ func Register(c *gin.Context) { }) return } - c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", @@ -163,7 +169,11 @@ func Register(c *gin.Context) { } func GetAllUsers(c *gin.Context) { - users, err := model.GetAllUsers() + p, _ := strconv.Atoi(c.Query("p")) + if p < 0 { + p = 0 + } + users, err := model.GetAllUsers(p*common.ItemsPerPage, common.ItemsPerPage) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -372,10 +382,6 @@ func UpdateSelf(c *gin.Context) { Username: user.Username, Password: user.Password, DisplayName: user.DisplayName, - Token: user.Token, - } - if cleanUser.Token == "" { - cleanUser.Token = " " // this is because gorm will ignore zero value } if user.Password == "$I_LOVE_U" { user.Password = "" // rollback to what it should be @@ -449,7 +455,6 @@ func DeleteSelf(c *gin.Context) { return } -// CreateUser Only admin user can call this, so we can trust it func CreateUser(c *gin.Context) { var user model.User err := json.NewDecoder(c.Request.Body).Decode(&user) @@ -471,8 +476,13 @@ func CreateUser(c *gin.Context) { }) return } - - if err := user.Insert(); err != nil { + // Even for admin users, we cannot fully trust them! + cleanUser := model.User{ + Username: user.Username, + Password: user.Password, + DisplayName: user.DisplayName, + } + if err := cleanUser.Insert(); err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), @@ -557,10 +567,14 @@ func ManageUser(c *gin.Context) { }) return } - + clearUser := model.User{ + Role: user.Role, + Status: user.Status, + } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", + "data": clearUser, }) return } @@ -571,7 +585,7 @@ func EmailBind(c *gin.Context) { if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "验证码错误!", + "message": "验证码错误或已过期", }) return } diff --git a/controller/wechat.go b/controller/wechat.go index 92a8e02..013d213 100644 --- a/controller/wechat.go +++ b/controller/wechat.go @@ -72,15 +72,23 @@ func WeChatAuth(c *gin.Context) { if model.IsWeChatIdAlreadyTaken(wechatId) { user.FillUserByWeChatId() } else { - user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1) - user.DisplayName = "WeChat User" - user.Role = common.RoleCommonUser - user.Status = common.UserStatusEnabled + if common.RegisterEnabled { + user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1) + user.DisplayName = "WeChat User" + user.Role = common.RoleCommonUser + user.Status = common.UserStatusEnabled - if err := user.Insert(); err != nil { + if err := user.Insert(); err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + } else { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": err.Error(), + "message": "管理员关闭了新用户注册", }) return } diff --git a/go.mod b/go.mod index b2e6552..f5c631d 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,15 @@ module message-pusher go 1.18 require ( + github.com/gin-contrib/cors v1.4.0 + github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/sessions v0.0.5 - github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 + github.com/gin-contrib/static v0.0.1 github.com/gin-gonic/gin v1.8.1 github.com/go-playground/validator/v10 v10.11.1 github.com/go-redis/redis/v8 v8.11.5 github.com/google/uuid v1.3.0 + github.com/yuin/goldmark v1.5.3 golang.org/x/crypto v0.1.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/mysql v1.4.3 @@ -39,9 +42,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect - github.com/stretchr/testify v1.8.0 // indirect github.com/ugorji/go/codec v1.2.7 // indirect - github.com/yuin/goldmark v1.5.2 // indirect golang.org/x/net v0.1.0 // indirect golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.4.0 // indirect diff --git a/go.sum b/go.sum index 74862fc..9533a8a 100644 --- a/go.sum +++ b/go.sum @@ -9,20 +9,29 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= +github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 h1:J2LPEOcQmWaooBnBtUDV9KHFEnP5LYTZY03GiQ0oQBw= -github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= +github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U= +github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= @@ -31,6 +40,7 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= @@ -51,6 +61,7 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -61,8 +72,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -70,6 +83,7 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -86,22 +100,28 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= -github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M= +github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -110,6 +130,8 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= @@ -130,6 +152,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index db84e1f..54e2a2f 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "embed" + "github.com/gin-contrib/gzip" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/redis" @@ -54,6 +55,7 @@ func main() { // Initialize HTTP server server := gin.Default() + server.Use(gzip.Gzip(gzip.DefaultCompression)) server.Use(middleware.CORS()) // Initialize session store @@ -71,16 +73,6 @@ func main() { if port == "" { port = strconv.Itoa(*common.Port) } - //if *common.Host == "localhost" { - // ip := common.GetIp() - // if ip != "" { - // *common.Host = ip - // } - //} - //serverUrl := "http://" + *common.Host + ":" + port + "/" - //if !*common.NoBrowser { - // common.OpenBrowser(serverUrl) - //} err = server.Run(":" + port) if err != nil { log.Println(err) diff --git a/middleware/auth.go b/middleware/auth.go index ef9dbd7..211e79a 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -4,6 +4,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "message-pusher/common" + "message-pusher/model" "net/http" ) @@ -13,13 +14,34 @@ func authHelper(c *gin.Context, minRole int) { role := session.Get("role") id := session.Get("id") status := session.Get("status") + authByToken := false if username == nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "无权进行此操作,用户未登录", - }) - c.Abort() - return + // Check token + token := c.Request.Header.Get("Authorization") + if token == "" { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无权进行此操作,未登录或 token 无效", + }) + c.Abort() + return + } + user := model.ValidateUserToken(token) + if user != nil && user.Username != "" { + // Token is valid + username = user.Username + role = user.Role + id = user.Id + status = user.Status + } else { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无权进行此操作,token 无效", + }) + c.Abort() + return + } + authByToken = true } if status.(int) == common.UserStatusDisabled { c.JSON(http.StatusOK, gin.H{ @@ -32,7 +54,7 @@ func authHelper(c *gin.Context, minRole int) { if role.(int) < minRole { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "无权进行此操作,用户未登录或没有权限", + "message": "无权进行此操作,权限不足", }) c.Abort() return @@ -40,6 +62,7 @@ func authHelper(c *gin.Context, minRole int) { c.Set("username", username) c.Set("role", role) c.Set("id", id) + c.Set("authByToken", authByToken) c.Next() } @@ -60,3 +83,35 @@ func RootAuth() func(c *gin.Context) { authHelper(c, common.RoleRootUser) } } + +// NoTokenAuth You should always use this after normal auth middlewares. +func NoTokenAuth() func(c *gin.Context) { + return func(c *gin.Context) { + authByToken := c.GetBool("authByToken") + if authByToken { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "本接口不支持使用 token 进行验证", + }) + c.Abort() + return + } + c.Next() + } +} + +// TokenOnlyAuth You should always use this after normal auth middlewares. +func TokenOnlyAuth() func(c *gin.Context) { + return func(c *gin.Context) { + authByToken := c.GetBool("authByToken") + if !authByToken { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "本接口仅支持使用 token 进行验证", + }) + c.Abort() + return + } + c.Next() + } +} diff --git a/middleware/cache.go b/middleware/cache.go new file mode 100644 index 0000000..7f6099f --- /dev/null +++ b/middleware/cache.go @@ -0,0 +1,12 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +func Cache() func(c *gin.Context) { + return func(c *gin.Context) { + c.Header("Cache-Control", "max-age=604800") // one week + c.Next() + } +} diff --git a/middleware/cors.go b/middleware/cors.go index 5d76edd..170cca3 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -1,20 +1,12 @@ package middleware import ( - "github.com/gin-gonic/contrib/cors" + "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "time" ) func CORS() gin.HandlerFunc { config := cors.DefaultConfig() - config.AllowedHeaders = []string{"Authorization", "Content-Type", "Origin", - "Connection", "Accept-Encoding", "Accept-Language", "Host"} - config.AllowedMethods = []string{"GET", "POST", "DELETE", "OPTIONS", "PUT"} - config.AllowCredentials = true - config.MaxAge = 12 * time.Hour - // if you want to allow all origins, comment the following two lines - config.AllowAllOrigins = false - config.AllowedOrigins = []string{"https://message-pusher.vercel.app"} + config.AllowOrigins = []string{"https://gin-template.vercel.app", "http://localhost:3000/"} return cors.New(config) } diff --git a/middleware/turnstile-check.go b/middleware/turnstile-check.go new file mode 100644 index 0000000..08c9f92 --- /dev/null +++ b/middleware/turnstile-check.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "encoding/json" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "message-pusher/common" + "net/http" + "net/url" +) + +type turnstileCheckResponse struct { + Success bool `json:"success"` +} + +func TurnstileCheck() gin.HandlerFunc { + return func(c *gin.Context) { + if common.TurnstileCheckEnabled { + session := sessions.Default(c) + turnstileChecked := session.Get("turnstile") + if turnstileChecked != nil { + c.Next() + return + } + response := c.Query("turnstile") + if response == "" { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "Turnstile token 为空", + }) + c.Abort() + return + } + rawRes, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", url.Values{ + "secret": {common.TurnstileSecretKey}, + "response": {response}, + "remoteip": {c.ClientIP()}, + }) + if err != nil { + common.SysError(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + c.Abort() + return + } + defer rawRes.Body.Close() + var res turnstileCheckResponse + err = json.NewDecoder(rawRes.Body).Decode(&res) + if err != nil { + common.SysError(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + c.Abort() + return + } + if !res.Success { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "Turnstile 校验失败,请刷新重试!", + }) + c.Abort() + return + } + session.Set("turnstile", true) + err = session.Save() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "message": "无法保存会话信息,请重试", + "success": false, + }) + return + } + } + c.Next() + } +} diff --git a/model/file.go b/model/file.go deleted file mode 100644 index 920cb26..0000000 --- a/model/file.go +++ /dev/null @@ -1,53 +0,0 @@ -package model - -import ( - _ "gorm.io/driver/sqlite" - "gorm.io/gorm" - "message-pusher/common" - "os" - "path" - "strings" -) - -type File struct { - Id int `json:"id"` - Filename string `json:"filename"` - Description string `json:"description"` - Uploader string `json:"uploader"` - Link string `json:"link" gorm:"unique"` - Time string `json:"time"` - DownloadCounter int `json:"download_counter"` -} - -func GetAllFiles() ([]*File, error) { - var files []*File - var err error - err = DB.Find(&files).Error - return files, err -} - -func QueryFiles(query string, startIdx int) ([]*File, error) { - var files []*File - var err error - query = strings.ToLower(query) - err = DB.Limit(common.ItemsPerPage).Offset(startIdx).Where("filename LIKE ? or description LIKE ? or uploader LIKE ? or time LIKE ?", "%"+query+"%", "%"+query+"%", "%"+query+"%", "%"+query+"%").Order("id desc").Find(&files).Error - return files, err -} - -func (file *File) Insert() error { - var err error - err = DB.Create(file).Error - return err -} - -// Delete Make sure link is valid! Because we will use os.Remove to delete it! -func (file *File) Delete() error { - var err error - err = DB.Delete(file).Error - err = os.Remove(path.Join(common.UploadPath, file.Link)) - return err -} - -func UpdateDownloadCounter(link string) { - DB.Model(&File{}).Where("link = ?", link).UpdateColumn("download_counter", gorm.Expr("download_counter + 1")) -} diff --git a/model/main.go b/model/main.go index dd35336..6a651f9 100644 --- a/model/main.go +++ b/model/main.go @@ -14,6 +14,7 @@ func createRootAccountIfNeed() error { var user User //if user.Status != common.UserStatusEnabled { if err := DB.First(&user).Error; err != nil { + common.SysLog("no user exists, create a root user for you: username is root, password is 123456") hashedPassword, err := common.Password2Hash("123456") if err != nil { return err @@ -51,11 +52,7 @@ func InitDB() (err error) { } if err == nil { DB = db - err := db.AutoMigrate(&File{}) - if err != nil { - return err - } - err = db.AutoMigrate(&User{}) + err := db.AutoMigrate(&User{}) if err != nil { return err } diff --git a/model/option.go b/model/option.go index 4690844..6437d8e 100644 --- a/model/option.go +++ b/model/option.go @@ -31,6 +31,8 @@ func InitOptionMap() { common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled) common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled) common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled) + common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled) + common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled) common.OptionMap["SMTPServer"] = "" common.OptionMap["SMTPAccount"] = "" common.OptionMap["SMTPToken"] = "" @@ -43,6 +45,8 @@ func InitOptionMap() { common.OptionMap["WeChatServerAddress"] = "" common.OptionMap["WeChatServerToken"] = "" common.OptionMap["WeChatAccountQRCodeImageURL"] = "" + common.OptionMap["TurnstileSiteKey"] = "" + common.OptionMap["TurnstileSecretKey"] = "" common.OptionMapRWMutex.Unlock() options, _ := AllOption() for _, option := range options { @@ -87,16 +91,26 @@ func updateOptionMap(key string, value string) { common.ImageDownloadPermission = intValue } } - boolValue := value == "true" + if strings.HasSuffix(key, "Enabled") { + boolValue := value == "true" + switch key { + case "PasswordRegisterEnabled": + common.PasswordRegisterEnabled = boolValue + case "PasswordLoginEnabled": + common.PasswordLoginEnabled = boolValue + case "EmailVerificationEnabled": + common.EmailVerificationEnabled = boolValue + case "GitHubOAuthEnabled": + common.GitHubOAuthEnabled = boolValue + case "WeChatAuthEnabled": + common.WeChatAuthEnabled = boolValue + case "TurnstileCheckEnabled": + common.TurnstileCheckEnabled = boolValue + case "RegisterEnabled": + common.RegisterEnabled = boolValue + } + } switch key { - case "PasswordRegisterEnabled": - common.PasswordRegisterEnabled = boolValue - case "PasswordLoginEnabled": - common.PasswordLoginEnabled = boolValue - case "EmailVerificationEnabled": - common.EmailVerificationEnabled = boolValue - case "GitHubOAuthEnabled": - common.GitHubOAuthEnabled = boolValue case "SMTPServer": common.SMTPServer = value case "SMTPAccount": @@ -117,7 +131,9 @@ func updateOptionMap(key string, value string) { common.WeChatServerToken = value case "WeChatAccountQRCodeImageURL": common.WeChatAccountQRCodeImageURL = value - case "WeChatAuthEnabled": - common.WeChatAuthEnabled = boolValue + case "TurnstileSiteKey": + common.TurnstileSiteKey = value + case "TurnstileSecretKey": + common.TurnstileSecretKey = value } } diff --git a/model/user.go b/model/user.go index 5432ced..ef67abd 100644 --- a/model/user.go +++ b/model/user.go @@ -3,8 +3,11 @@ package model import ( "errors" "message-pusher/common" + "strings" ) +// User if you add sensitive fields, don't forget to clean them in setupLogin function. +// Otherwise, the sensitive information will be saved on local storage in plain text! type User struct { Id int `json:"id"` Username string `json:"username" gorm:"unique;index" validate:"max=12"` @@ -12,12 +15,12 @@ type User struct { DisplayName string `json:"display_name" gorm:"index" validate:"max=20"` Role int `json:"role" gorm:"type:int;default:1"` // admin, common Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled - Token string `json:"token"` + Token string `json:"token" gorm:"index"` Email string `json:"email" gorm:"index" validate:"max=50"` GitHubId string `json:"github_id" gorm:"column:github_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` + VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! Channel string `json:"channel"` - VerificationCode string `json:"verification_code" gorm:"-:all"` WeChatTestAccountId string `json:"wechat_test_account_id" gorm:"column:wechat_test_account_id"` WeChatTestAccountSecret string `json:"wechat_test_account_secret" gorm:"column:wechat_test_account_secret"` WeChatTestAccountTemplateId string `json:"wechat_test_account_template_id" gorm:"column:wechat_test_account_template_id"` @@ -40,8 +43,8 @@ func GetMaxUserId() int { return user.Id } -func GetAllUsers() (users []*User, err error) { - err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email"}).Find(&users).Error +func GetAllUsers(startIdx int, num int) (users []*User, err error) { + err = DB.Order("id desc").Limit(num).Offset(startIdx).Select([]string{"id", "username", "display_name", "role", "status", "email"}).Find(&users).Error return users, err } @@ -61,7 +64,7 @@ func GetUserById(id int, selectAll bool) (*User, error) { if selectAll { err = DB.First(&user, "id = ?", id).Error } else { - err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email", "wechat_id", "github_id", "token"}).First(&user, "id = ?", id).Error + err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email", "wechat_id", "github_id"}).First(&user, "id = ?", id).Error } return &user, err } @@ -108,10 +111,13 @@ func (user *User) ValidateAndFill() (err error) { // that means if your field’s value is 0, '', false or other zero values, // it won’t be used to build query conditions password := user.Password + if password == "" { + return errors.New("密码为空") + } DB.Where(User{Username: user.Username}).First(user) okay := common.ValidatePasswordAndHash(password, user.Password) if !okay || user.Status != common.UserStatusEnabled { - return errors.New("用户名或密码错误,或者该用户已被封禁") + return errors.New("用户名或密码错误,或用户已被封禁") } return nil } @@ -136,6 +142,18 @@ func (user *User) FillUserByUsername() { DB.Where(User{Username: user.Username}).First(user) } +func ValidateUserToken(token string) (user *User) { + if token == "" { + return nil + } + token = strings.Replace(token, "Bearer ", "", 1) + user = &User{} + if DB.Where("token = ?", token).First(user).RowsAffected == 1 { + return user + } + return nil +} + func IsEmailAlreadyTaken(email string) bool { return DB.Where("email = ?", email).Find(&User{}).RowsAffected == 1 } diff --git a/router/api-router.go b/router/api-router.go index 774159d..f1b1bef 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -13,8 +13,8 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/status", controller.GetStatus) apiRouter.GET("/notice", controller.GetNotice) apiRouter.GET("/about", controller.GetAbout) - apiRouter.GET("/verification", middleware.CriticalRateLimit(), controller.SendEmailVerification) - apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), controller.SendPasswordResetEmail) + apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) + apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth) apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) @@ -23,12 +23,12 @@ func SetApiRouter(router *gin.Engine) { userRoute := apiRouter.Group("/user") { - userRoute.POST("/register", middleware.CriticalRateLimit(), controller.Register) + userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register) userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login) userRoute.GET("/logout", controller.Logout) selfRoute := userRoute.Group("/") - selfRoute.Use(middleware.UserAuth()) + selfRoute.Use(middleware.UserAuth(), middleware.NoTokenAuth()) { selfRoute.GET("/self", controller.GetSelf) selfRoute.PUT("/self", controller.UpdateSelf) @@ -37,7 +37,7 @@ func SetApiRouter(router *gin.Engine) { } adminRoute := userRoute.Group("/") - adminRoute.Use(middleware.AdminAuth()) + adminRoute.Use(middleware.AdminAuth(), middleware.NoTokenAuth()) { adminRoute.GET("/", controller.GetAllUsers) adminRoute.GET("/search", controller.SearchUsers) @@ -49,17 +49,11 @@ func SetApiRouter(router *gin.Engine) { } } optionRoute := apiRouter.Group("/option") - optionRoute.Use(middleware.RootAuth()) + optionRoute.Use(middleware.RootAuth(), middleware.NoTokenAuth()) { optionRoute.GET("/", controller.GetOptions) optionRoute.PUT("/", controller.UpdateOption) } - fileRoute := apiRouter.Group("/file") - { - fileRoute.GET("/:id", middleware.DownloadRateLimit(), controller.DownloadFile) - fileRoute.POST("/", middleware.UserAuth(), middleware.UploadRateLimit(), controller.UploadFile) - fileRoute.DELETE("/:id", middleware.UserAuth(), controller.DeleteFile) - } } pushRouter := router.Group("/push") pushRouter.Use(middleware.GlobalAPIRateLimit()) diff --git a/router/web-router.go b/router/web-router.go index 9b95779..1db50fe 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -2,7 +2,7 @@ package router import ( "embed" - "github.com/gin-gonic/contrib/static" + "github.com/gin-contrib/static" "github.com/gin-gonic/gin" "message-pusher/common" "message-pusher/middleware" @@ -11,6 +11,7 @@ import ( func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { router.Use(middleware.GlobalWebRateLimit()) + router.Use(middleware.Cache()) router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) router.NoRoute(func(c *gin.Context) { c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage) diff --git a/web/package.json b/web/package.json index ae2bfcb..4692321 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-toastify": "^9.0.8", + "react-turnstile": "^1.0.5", "semantic-ui-css": "^2.5.0", "semantic-ui-react": "^2.1.3" }, diff --git a/web/src/App.js b/web/src/App.js index 220767c..05cd349 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,4 +1,4 @@ -import React, { lazy, Suspense, useEffect } from 'react'; +import React, { lazy, Suspense, useContext, useEffect } from 'react'; import { Route, Routes } from 'react-router-dom'; import Loading from './components/Loading'; import User from './pages/User'; @@ -9,34 +9,52 @@ import NotFound from './pages/NotFound'; import Setting from './pages/Setting'; import EditUser from './pages/User/EditUser'; import AddUser from './pages/User/AddUser'; -import { API, showError } from './helpers'; +import { API, showError, showNotice } from './helpers'; import PasswordResetForm from './components/PasswordResetForm'; import GitHubOAuth from './components/GitHubOAuth'; import PasswordResetConfirm from './components/PasswordResetConfirm'; +import { UserContext } from './context/User'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); function App() { + const [userState, userDispatch] = useContext(UserContext); + + const loadUser = () => { + let user = localStorage.getItem('user'); + if (user) { + let data = JSON.parse(user); + userDispatch({ type: 'login', payload: data }); + } + }; const loadStatus = async () => { const res = await API.get('/api/status'); const { success, data } = res.data; if (success) { localStorage.setItem('status', JSON.stringify(data)); localStorage.setItem('footer_html', data.footer_html); + let currentVersion = localStorage.getItem('version'); + if (currentVersion && currentVersion !== data.version) { + localStorage.setItem('version', data.version); + showNotice( + `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面` + ); + } } else { showError('无法正常连接至服务器!'); } }; useEffect(() => { + loadUser(); loadStatus().then(); }, []); return ( }> @@ -44,7 +62,7 @@ function App() { } /> @@ -52,7 +70,7 @@ function App() { } /> }> @@ -60,7 +78,7 @@ function App() { } /> }> @@ -68,7 +86,7 @@ function App() { } /> }> @@ -76,7 +94,7 @@ function App() { } /> }> @@ -84,7 +102,7 @@ function App() { } /> }> @@ -92,7 +110,7 @@ function App() { } /> }> @@ -100,7 +118,7 @@ function App() { } /> }> @@ -108,7 +126,7 @@ function App() { } /> }> @@ -116,7 +134,7 @@ function App() { } /> }> @@ -126,14 +144,14 @@ function App() { } /> }> } /> - + ); } diff --git a/web/src/components/Footer.js b/web/src/components/Footer.js index 0583eaa..44b8a50 100644 --- a/web/src/components/Footer.js +++ b/web/src/components/Footer.js @@ -12,27 +12,27 @@ const Footer = () => { return ( - + {Footer === '' ? ( -
+ ) : (
)} diff --git a/web/src/components/Header.js b/web/src/components/Header.js index 51f32bb..58c845d 100644 --- a/web/src/components/Header.js +++ b/web/src/components/Header.js @@ -2,7 +2,7 @@ import React, { useContext, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { UserContext } from '../context/User'; -import { Button, Container, Icon, Menu, Segment } from 'semantic-ui-react'; +import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react'; import { API, isAdmin, isMobile, showSuccess } from '../helpers'; import '../index.css'; @@ -34,7 +34,6 @@ const headerButtons = [ const Header = () => { const [userState, userDispatch] = useContext(UserContext); let navigate = useNavigate(); - let size = isMobile() ? 'large' : ''; const [showSidebar, setShowSidebar] = useState(false); @@ -80,7 +79,7 @@ const Header = () => { <> { } > - + logo
消息推送服务
- + @@ -149,28 +148,32 @@ const Header = () => { return ( <> - + - - logo + + logo
消息推送服务
{renderButtons(false)} - + {userState.user ? ( - + + + 注销 + + ) : ( )} diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index ed2adb5..e23aaca 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -114,7 +114,7 @@ const LoginForm = () => { value={password} onChange={handleChange} /> - @@ -179,7 +179,7 @@ const LoginForm = () => { onChange={handleChange} /> + + +
账号绑定
@@ -123,7 +157,8 @@ const PersonalSetting = () => { onClose={() => setShowEmailBindModal(false)} onOpen={() => setShowEmailBindModal(true)} open={showEmailBindModal} - size={'mini'} + size={'tiny'} + style={{ maxWidth: '450px' }} > 绑定邮箱地址 @@ -136,7 +171,9 @@ const PersonalSetting = () => { name='email' type='email' action={ - + } /> { value={inputs.email_verification_code} onChange={handleInputChange} /> - diff --git a/web/src/components/RegisterForm.js b/web/src/components/RegisterForm.js index 8b52714..aa6d22f 100644 --- a/web/src/components/RegisterForm.js +++ b/web/src/components/RegisterForm.js @@ -10,6 +10,7 @@ import { } from 'semantic-ui-react'; import { Link, useNavigate } from 'react-router-dom'; import { API, showError, showInfo, showSuccess } from '../helpers'; +import Turnstile from 'react-turnstile'; const RegisterForm = () => { const [inputs, setInputs] = useState({ @@ -20,14 +21,21 @@ const RegisterForm = () => { verification_code: '', }); const { username, password, password2 } = inputs; - const [showEmailVerification, setShowEmailVerification] = useState(false); + const [turnstileEnabled, setTurnstileEnabled] = useState(false); + const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); + const [turnstileToken, setTurnstileToken] = useState(''); + const [loading, setLoading] = useState(false); useEffect(() => { let status = localStorage.getItem('status'); if (status) { status = JSON.parse(status); setShowEmailVerification(status.email_verification); + if (status.turnstile_check) { + setTurnstileEnabled(true); + setTurnstileSiteKey(status.turnstile_site_key); + } } }); @@ -49,7 +57,15 @@ const RegisterForm = () => { return; } if (username && password) { - const res = await API.post('/api/user/register', inputs); + if (turnstileEnabled && turnstileToken === '') { + showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); + return; + } + setLoading(true); + const res = await API.post( + `/api/user/register?turnstile=${turnstileToken}`, + inputs + ); const { success, message } = res.data; if (success) { navigate('/login'); @@ -57,18 +73,27 @@ const RegisterForm = () => { } else { showError(message); } + setLoading(false); } } const sendVerificationCode = async () => { if (inputs.email === '') return; - const res = await API.get(`/api/verification?email=${inputs.email}`); + if (turnstileEnabled && turnstileToken === '') { + showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); + return; + } + setLoading(true); + const res = await API.get( + `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` + ); const { success, message } = res.data; if (success) { showSuccess('验证码发送成功,请检查你的邮箱!'); } else { showError(message); } + setLoading(false); }; return ( @@ -116,7 +141,9 @@ const RegisterForm = () => { name='email' type='email' action={ - + } /> { ) : ( <> )} - diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 006cc29..b49ff72 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Form, Grid } from 'semantic-ui-react'; +import { Divider, Form, Grid, Header } from 'semantic-ui-react'; import { API, showError } from '../helpers'; const SystemSetting = () => { @@ -20,6 +20,10 @@ const SystemSetting = () => { WeChatServerAddress: '', WeChatServerToken: '', WeChatAccountQRCodeImageURL: '', + TurnstileCheckEnabled: '', + TurnstileSiteKey: '', + TurnstileSecretKey: '', + RegisterEnabled: '', }); let originInputs = {}; let [loading, setLoading] = useState(false); @@ -51,6 +55,8 @@ const SystemSetting = () => { case 'EmailVerificationEnabled': case 'GitHubOAuthEnabled': case 'WeChatAuthEnabled': + case 'TurnstileCheckEnabled': + case 'RegisterEnabled': value = inputs[key] === 'true' ? 'false' : 'true'; break; default: @@ -78,7 +84,9 @@ const SystemSetting = () => { name === 'GitHubClientSecret' || name === 'WeChatServerAddress' || name === 'WeChatServerToken' || - name === 'WeChatAccountQRCodeImageURL' + name === 'WeChatAccountQRCodeImageURL' || + name === 'TurnstileSiteKey' || + name === 'TurnstileSecretKey' ) { setInputs((inputs) => ({ ...inputs, [name]: value })); } else { @@ -142,125 +150,226 @@ const SystemSetting = () => { } }; + const submitTurnstile = async () => { + if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { + await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); + } + if ( + originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && + inputs.TurnstileSecretKey !== '' + ) { + await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey); + } + }; + return (
- +
通用设置
+ 更新服务器地址 + +
配置登录注册
+ + + + + +
+ 配置 SMTP + 用以支持系统的邮件发送 +
保存 SMTP 设置 + +
+ 配置 GitHub OAuth App + + 用以支持通过 GitHub 进行登录注册, + + 点击此处 + + 管理你的 GitHub OAuth App + +
保存 GitHub OAuth 设置 + +
+ 配置 WeChat Server + + 用以支持通过微信进行登录注册, + + 点击此处 + + 了解 WeChat Server + +
- 保存微信登录设置 + + 保存 WeChat Server 设置 + + +
+ 配置 Turnstile + + 用以支持用户校验, + + 点击此处 + + 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type + +
+ + + + + + 保存 Turnstile 设置 +
diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index bb79820..ac6891a 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -3,18 +3,18 @@ import { Button, Form, Label, Pagination, Table } from 'semantic-ui-react'; import { Link } from 'react-router-dom'; import { API, showError, showSuccess } from '../helpers'; -const itemsPerPage = 10; +import { ITEMS_PER_PAGE } from '../constants'; function renderRole(role) { switch (role) { case 1: return ; case 10: - return ; + return ; case 100: - return ; + return ; default: - return ; + return ; } } @@ -25,11 +25,17 @@ const UsersTable = () => { const [searchKeyword, setSearchKeyword] = useState(''); const [searching, setSearching] = useState(false); - const loadUsers = async () => { - const res = await API.get('/api/user'); + const loadUsers = async (startIdx) => { + const res = await API.get(`/api/user/?p=${startIdx}`); const { success, message, data } = res.data; if (success) { - setUsers(data); + if (startIdx === 0) { + setUsers(data); + } else { + let newUsers = users; + newUsers.push(...data); + setUsers(newUsers); + } } else { showError(message); } @@ -37,18 +43,24 @@ const UsersTable = () => { }; const onPaginationChange = (e, { activePage }) => { - setActivePage(activePage); + (async () => { + if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + await loadUsers(activePage - 1); + } + setActivePage(activePage); + })(); }; useEffect(() => { - loadUsers() + loadUsers(0) .then() .catch((reason) => { showError(reason); }); }, []); - const manageUser = (username, action) => { + const manageUser = (username, action, idx) => { (async () => { const res = await API.post('/api/user/manage', { username, @@ -57,38 +69,62 @@ const UsersTable = () => { const { success, message } = res.data; if (success) { showSuccess('操作成功完成!'); - await loadUsers(); + let user = res.data.data; + let newUsers = [...users]; + let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; + if (action === 'delete') { + newUsers[realIdx].deleted = true; + } else { + newUsers[realIdx].status = user.status; + newUsers[realIdx].role = user.role; + } + setUsers(newUsers); } else { showError(message); } })(); }; - const renderStatus = (status, id) => { + const renderStatus = (status) => { switch (status) { case 1: - return '已激活'; + return ; case 2: - return '已封禁'; + return ( + + ); default: - return '未知状态'; + return ( + + ); } }; const searchUsers = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadUsers(0); + setActivePage(1); + return; + } setSearching(true); const res = await API.get(`/api/user/search?keyword=${searchKeyword}`); const { success, message, data } = res.data; if (success) { setUsers(data); + setActivePage(1); } else { showError(message); } setSearching(false); }; - const handleKeywordChange = async (e, { name, value }) => { - setSearchKeyword(value); + const handleKeywordChange = async (e, { value }) => { + setSearchKeyword(value.trim()); }; const sortUser = (key) => { @@ -109,10 +145,10 @@ const UsersTable = () => { <>
{ {users - .slice((activePage - 1) * itemsPerPage, activePage * itemsPerPage) + .slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE + ) .map((user, idx) => { + if (user.deleted) return <>; return ( {user.username} {user.display_name} {user.email ? user.email : '无'} {renderRole(user.role)} - {renderStatus(user.status, user.id)} + {renderStatus(user.status)}
diff --git a/web/src/constants/common.constant.js b/web/src/constants/common.constant.js new file mode 100644 index 0000000..1a37d5f --- /dev/null +++ b/web/src/constants/common.constant.js @@ -0,0 +1 @@ +export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend! diff --git a/web/src/constants/index.js b/web/src/constants/index.js index 6992a8e..8f9ba80 100644 --- a/web/src/constants/index.js +++ b/web/src/constants/index.js @@ -1,2 +1,3 @@ export * from './toast.constants'; -export * from './user.constants'; \ No newline at end of file +export * from './user.constants'; +export * from './common.constant'; \ No newline at end of file diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index 4b6c023..c80c9aa 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -85,3 +85,7 @@ export function showInfo(message) { export function showNotice(message) { toast.info(message, showNoticeOptions); } + +export function openPage(url) { + window.open(url); +} diff --git a/web/src/pages/About/index.js b/web/src/pages/About/index.js index 0dd2881..d6bfd77 100644 --- a/web/src/pages/About/index.js +++ b/web/src/pages/About/index.js @@ -26,18 +26,20 @@ const About = () => { return ( <> - { - about === '' ? <> + {about === '' ? ( + <>
关于

可在设置页面设置关于内容,支持 HTML & Markdown

项目仓库地址: - + https://github.com/songquanpeng/message-pusher - : <> -
- } + ) : ( + <> +
+ + )}
); diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index d7289a7..d667097 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -13,18 +13,9 @@ const EditUser = () => { password: '', github_id: '', wechat_id: '', - email: '', - token: '', + email:'' }); - const { - username, - display_name, - password, - github_id, - wechat_id, - email, - token, - } = inputs; + const { username, display_name, password, github_id, wechat_id, email } = inputs; const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; @@ -39,9 +30,6 @@ const EditUser = () => { const { success, message, data } = res.data; if (success) { data.password = ''; - if (data.token === ' ') { - data.token = ''; - } setInputs(data); } else { showError(message); @@ -70,77 +58,64 @@ const EditUser = () => { return ( <> -
更新用户信息
- +
更新用户信息
+ - - -