chore: sync with gin-template

This commit is contained in:
JustSong
2022-11-22 11:04:06 +08:00
parent 8247192977
commit 5913266969
41 changed files with 813 additions and 490 deletions
+5
View File
@@ -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
+1 -3
View File
@@ -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()
+1 -1
View File
@@ -2,7 +2,7 @@ package common
import (
"embed"
"github.com/gin-gonic/contrib/static"
"github.com/gin-contrib/static"
"io/fs"
"net/http"
)
+3 -3
View File
@@ -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")
)
+1 -1
View File
@@ -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 {
-131
View File
@@ -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)
}()
}
+8
View File
@@ -107,6 +107,7 @@ func GitHubOAuth(c *gin.Context) {
if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
user.FillUserByGitHubId()
} else {
if common.RegisterEnabled {
user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
user.DisplayName = githubUser.Name
user.Email = githubUser.Email
@@ -120,6 +121,13 @@ func GitHubOAuth(c *gin.Context) {
})
return
}
} else {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员关闭了新用户注册",
})
return
}
}
if user.Status != common.UserStatusEnabled {
+2
View File
@@ -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
+6
View File
@@ -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 {
+27 -13
View File
@@ -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
}
+8
View File
@@ -72,6 +72,7 @@ func WeChatAuth(c *gin.Context) {
if model.IsWeChatIdAlreadyTaken(wechatId) {
user.FillUserByWeChatId()
} else {
if common.RegisterEnabled {
user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
user.DisplayName = "WeChat User"
user.Role = common.RoleCommonUser
@@ -84,6 +85,13 @@ func WeChatAuth(c *gin.Context) {
})
return
}
} else {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员关闭了新用户注册",
})
return
}
}
if user.Status != common.UserStatusEnabled {
+4 -3
View File
@@ -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
+28 -4
View File
@@ -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=
+2 -10
View File
@@ -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)
+57 -2
View File
@@ -4,6 +4,7 @@ import (
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"message-pusher/common"
"message-pusher/model"
"net/http"
)
@@ -13,14 +14,35 @@ func authHelper(c *gin.Context, minRole int) {
role := session.Get("role")
id := session.Get("id")
status := session.Get("status")
authByToken := false
if username == nil {
// Check token
token := c.Request.Header.Get("Authorization")
if token == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无权进行此操作,用户未登录",
"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{
"success": false,
@@ -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()
}
}
+12
View File
@@ -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()
}
}
+2 -10
View File
@@ -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)
}
+80
View File
@@ -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()
}
}
-53
View File
@@ -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"))
}
+2 -5
View File
@@ -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
}
+18 -2
View File
@@ -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,6 +91,7 @@ func updateOptionMap(key string, value string) {
common.ImageDownloadPermission = intValue
}
}
if strings.HasSuffix(key, "Enabled") {
boolValue := value == "true"
switch key {
case "PasswordRegisterEnabled":
@@ -97,6 +102,15 @@ func updateOptionMap(key string, value string) {
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 "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
}
}
+24 -6
View File
@@ -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 fields value is 0, '', false or other zero values,
// it wont 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
}
+6 -12
View File
@@ -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())
+2 -1
View File
@@ -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)
+1
View File
@@ -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"
},
+33 -15
View File
@@ -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 (
<Routes>
<Route
path="/"
path='/'
element={
<Suspense fallback={<Loading></Loading>}>
<Home />
@@ -44,7 +62,7 @@ function App() {
}
/>
<Route
path="/user"
path='/user'
element={
<PrivateRoute>
<User />
@@ -52,7 +70,7 @@ function App() {
}
/>
<Route
path="/user/edit/:id"
path='/user/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<EditUser />
@@ -60,7 +78,7 @@ function App() {
}
/>
<Route
path="/user/edit"
path='/user/edit'
element={
<Suspense fallback={<Loading></Loading>}>
<EditUser />
@@ -68,7 +86,7 @@ function App() {
}
/>
<Route
path="/user/add"
path='/user/add'
element={
<Suspense fallback={<Loading></Loading>}>
<AddUser />
@@ -76,7 +94,7 @@ function App() {
}
/>
<Route
path="/user/reset"
path='/user/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetConfirm />
@@ -84,7 +102,7 @@ function App() {
}
/>
<Route
path="/login"
path='/login'
element={
<Suspense fallback={<Loading></Loading>}>
<LoginForm />
@@ -92,7 +110,7 @@ function App() {
}
/>
<Route
path="/register"
path='/register'
element={
<Suspense fallback={<Loading></Loading>}>
<RegisterForm />
@@ -100,7 +118,7 @@ function App() {
}
/>
<Route
path="/reset"
path='/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetForm />
@@ -108,7 +126,7 @@ function App() {
}
/>
<Route
path="/oauth/github"
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>}>
<GitHubOAuth />
@@ -116,7 +134,7 @@ function App() {
}
/>
<Route
path="/setting"
path='/setting'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
@@ -126,14 +144,14 @@ function App() {
}
/>
<Route
path="/about"
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<About />
</Suspense>
}
/>
<Route path="*" element={NotFound} />
<Route path='*' element={NotFound} />
</Routes>
);
}
+7 -7
View File
@@ -12,27 +12,27 @@ const Footer = () => {
return (
<Segment vertical>
<Container textAlign="center">
<Container textAlign='center'>
{Footer === '' ? (
<div className="custom-footer">
<div className='custom-footer'>
<a
href="https://github.com/songquanpeng/message-pusher"
target="_blank"
href='https://github.com/songquanpeng/message-pusher'
target='_blank'
>
消息推送服务 {process.env.REACT_APP_VERSION}{' '}
</a>
{' '}
<a href="https://github.com/songquanpeng" target="_blank">
<a href='https://github.com/songquanpeng' target='_blank'>
JustSong
</a>{' '}
构建源代码遵循{' '}
<a href="https://opensource.org/licenses/mit-license.php">
<a href='https://opensource.org/licenses/mit-license.php'>
MIT 协议
</a>
</div>
) : (
<div
className="custom-footer"
className='custom-footer'
dangerouslySetInnerHTML={{ __html: Footer }}
></div>
)}
+22 -19
View File
@@ -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 = () => {
<>
<Menu
borderless
size={size}
size='large'
style={
showSidebar
? {
@@ -93,17 +92,17 @@ const Header = () => {
}
>
<Container>
<Menu.Item as={Link} to="/">
<Menu.Item as={Link} to='/'>
<img
src="/logo.png"
alt="logo"
src='/logo.png'
alt='logo'
style={{ marginRight: '0.75em' }}
/>
<div style={{ fontSize: '20px' }}>
<b>消息推送服务</b>
</div>
</Menu.Item>
<Menu.Menu position="right">
<Menu.Menu position='right'>
<Menu.Item onClick={toggleSidebar}>
<Icon name={showSidebar ? 'close' : 'sidebar'} />
</Menu.Item>
@@ -149,28 +148,32 @@ const Header = () => {
return (
<>
<Menu borderless size={size} style={{ borderTop: 'none' }}>
<Menu borderless style={{ borderTop: 'none' }}>
<Container>
<Menu.Item as={Link} to="/" className={'hide-on-mobile'}>
<img src="/logo.png" alt="logo" style={{ marginRight: '0.75em' }} />
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
<img src='/logo.png' alt='logo' style={{ marginRight: '0.75em' }} />
<div style={{ fontSize: '20px' }}>
<b>消息推送服务</b>
</div>
</Menu.Item>
{renderButtons(false)}
<Menu.Menu position="right">
<Menu.Menu position='right'>
{userState.user ? (
<Menu.Item
name="注销"
onClick={logout}
className="btn btn-link"
/>
<Dropdown
text={userState.user.username}
pointing
className='link item'
>
<Dropdown.Menu>
<Dropdown.Item onClick={logout}>注销</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
) : (
<Menu.Item
name="登录"
name='登录'
as={Link}
to="/login"
className="btn btn-link"
to='/login'
className='btn btn-link'
/>
)}
</Menu.Menu>
+2 -2
View File
@@ -114,7 +114,7 @@ const LoginForm = () => {
value={password}
onChange={handleChange}
/>
<Button color='teal' fluid size='large' onClick={handleSubmit}>
<Button color='telegram' fluid size='large' onClick={handleSubmit}>
登录
</Button>
</Segment>
@@ -179,7 +179,7 @@ const LoginForm = () => {
onChange={handleChange}
/>
<Button
color='teal'
color='telegram'
fluid
size='large'
onClick={onSubmitWeChatVerificationCode}
+10 -11
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Modal } from 'semantic-ui-react';
import { Button, Divider, Form, Grid, Header, Modal } from 'semantic-ui-react';
import { API, showError, showSuccess } from '../helpers';
import { marked } from 'marked';
@@ -7,7 +7,7 @@ const OtherSetting = () => {
let [inputs, setInputs] = useState({
Footer: '',
Notice: '',
About: ''
About: '',
});
let originInputs = {};
let [loading, setLoading] = useState(false);
@@ -42,7 +42,7 @@ const OtherSetting = () => {
setLoading(true);
const res = await API.put('/api/option', {
key,
value
value,
});
const { success, message } = res.data;
if (success) {
@@ -76,7 +76,7 @@ const OtherSetting = () => {
const checkUpdate = async () => {
const res = await API.get(
'https://api.github.com/repos/songquanpeng/message-pusher/releases/latest'
'https://api.github.com/repos/songquanpeng/gin-template/releases/latest'
);
const { tag_name, body } = res.data;
if (tag_name === process.env.REACT_APP_VERSION) {
@@ -94,6 +94,7 @@ const OtherSetting = () => {
<Grid columns={1}>
<Grid.Column>
<Form loading={loading}>
<Header as='h3'>通用设置</Header>
<Form.Button onClick={checkUpdate}>检查更新</Form.Button>
<Form.Group widths='equal'>
<Form.TextArea
@@ -106,6 +107,8 @@ const OtherSetting = () => {
/>
</Form.Group>
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
<Divider />
<Header as='h3'>个性化设置</Header>
<Form.Group widths='equal'>
<Form.TextArea
label='关于'
@@ -126,9 +129,7 @@ const OtherSetting = () => {
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitFooter}>
设置页脚
</Form.Button>
<Form.Button onClick={submitFooter}>设置页脚</Form.Button>
</Form>
</Grid.Column>
<Modal
@@ -139,15 +140,13 @@ const OtherSetting = () => {
<Modal.Header>新版本{updateData.tag_name}</Modal.Header>
<Modal.Content>
<Modal.Description>
<div
dangerouslySetInnerHTML={{ __html: updateData.content }}
></div>
<div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setShowUpdateModal(false)}>关闭</Button>
<Button
content="详情"
content='详情'
onClick={() => {
setShowUpdateModal(false);
openGitHubRelease();
+2 -2
View File
@@ -33,7 +33,7 @@ const PasswordResetConfirm = () => {
if (success) {
let password = res.data.data;
await copy(password);
showSuccess(`密码已重置并已复制到剪板:${password}`);
showSuccess(`密码已重置并已复制到剪板:${password}`);
} else {
showError(message);
}
@@ -58,7 +58,7 @@ const PasswordResetConfirm = () => {
readOnly
/>
<Button
color='teal'
color='telegram'
fluid
size='large'
onClick={handleSubmit}
+35 -4
View File
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
import { API, showError, showSuccess } from '../helpers';
import { API, showError, showInfo, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile';
const PasswordResetForm = () => {
const [inputs, setInputs] = useState({
@@ -9,6 +10,20 @@ const PasswordResetForm = () => {
const { email } = inputs;
const [loading, setLoading] = useState(false);
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState('');
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
if (status.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
}
}, []);
function handleChange(e) {
const { name, value } = e.target;
@@ -17,8 +32,14 @@ const PasswordResetForm = () => {
async function handleSubmit(e) {
if (!email) return;
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
return;
}
setLoading(true);
const res = await API.get(`/api/reset_password?email=${email}`);
const res = await API.get(
`/api/reset_password?email=${email}&turnstile=${turnstileToken}`
);
const { success, message } = res.data;
if (success) {
showSuccess('重置邮件发送成功,请检查邮箱!');
@@ -46,8 +67,18 @@ const PasswordResetForm = () => {
value={email}
onChange={handleChange}
/>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Button
color='teal'
color='telegram'
fluid
size='large'
onClick={handleSubmit}
+61 -8
View File
@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Image, Modal } from 'semantic-ui-react';
import { Button, Divider, Form, Header, Image, Modal } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers';
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile';
const PersonalSetting = () => {
const [inputs, setInputs] = useState({
@@ -12,12 +13,20 @@ const PersonalSetting = () => {
const [status, setStatus] = useState({});
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
const [showEmailBindModal, setShowEmailBindModal] = 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);
setStatus(status);
if (status.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
}
}, []);
@@ -25,6 +34,17 @@ const PersonalSetting = () => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const generateToken = async () => {
const res = await API.get('/api/user/token');
const { success, message, data } = res.data;
if (success) {
await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板:${data}`);
} else {
showError(message);
}
};
const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return;
const res = await API.get(
@@ -47,17 +67,26 @@ const PersonalSetting = () => {
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('验证码发送成功,请检查你的邮箱!');
showSuccess('验证码发送成功,请检查邮箱!');
} else {
showError(message);
}
setLoading(false);
};
const bindEmail = async () => {
if (inputs.email_verification_code === '') return;
setLoading(true);
const res = await API.get(
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
);
@@ -68,13 +97,18 @@ const PersonalSetting = () => {
} else {
showError(message);
}
setLoading(false);
};
return (
<div style={{ lineHeight: '40px' }}>
<Header as='h3'>通用设置</Header>
<Button as={Link} to={`/user/edit/`}>
更新个人信息
</Button>
<Button onClick={generateToken}>生成访问令牌</Button>
<Divider />
<Header as='h3'>账号绑定</Header>
<Button
onClick={() => {
setShowWeChatBindModal(true);
@@ -104,7 +138,7 @@ const PersonalSetting = () => {
value={inputs.wechat_verification_code}
onChange={handleInputChange}
/>
<Button color='teal' fluid size='large' onClick={bindWeChat}>
<Button color='telegram' fluid size='large' onClick={bindWeChat}>
绑定
</Button>
</Form>
@@ -123,7 +157,8 @@ const PersonalSetting = () => {
onClose={() => setShowEmailBindModal(false)}
onOpen={() => setShowEmailBindModal(true)}
open={showEmailBindModal}
size={'mini'}
size={'tiny'}
style={{ maxWidth: '450px' }}
>
<Modal.Header>绑定邮箱地址</Modal.Header>
<Modal.Content>
@@ -136,7 +171,9 @@ const PersonalSetting = () => {
name='email'
type='email'
action={
<Button onClick={sendVerificationCode}>获取验证码</Button>
<Button onClick={sendVerificationCode} disabled={loading}>
获取验证码
</Button>
}
/>
<Form.Input
@@ -146,7 +183,23 @@ const PersonalSetting = () => {
value={inputs.email_verification_code}
onChange={handleInputChange}
/>
<Button color='teal' fluid size='large' onClick={bindEmail}>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Button
color='telegram'
fluid
size='large'
onClick={bindEmail}
loading={loading}
>
绑定
</Button>
</Form>
+48 -5
View File
@@ -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={
<Button onClick={sendVerificationCode}>获取验证码</Button>
<Button onClick={sendVerificationCode} disabled={loading}>
获取验证码
</Button>
}
/>
<Form.Input
@@ -131,7 +158,23 @@ const RegisterForm = () => {
) : (
<></>
)}
<Button color='teal' fluid size='large' onClick={handleSubmit}>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Button
color='telegram'
fluid
size='large'
onClick={handleSubmit}
loading={loading}
>
注册
</Button>
</Segment>
+154 -45
View File
@@ -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 (
<Grid columns={1}>
<Grid.Column>
<Form loading={loading}>
<Form.Group widths="equal">
<Header as='h3'>通用设置</Header>
<Form.Group widths='equal'>
<Form.Input
label="服务器地址"
placeholder="例如:https://yourdomain.com(注意没有最后的斜杠)"
label='服务器地址'
placeholder='例如:https://yourdomain.com(注意没有最后的斜杠)'
value={inputs.ServerAddress}
name="ServerAddress"
name='ServerAddress'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitServerAddress}>
更新服务器地址
</Form.Button>
<Divider />
<Header as='h3'>配置登录注册</Header>
<Form.Group inline>
<Form.Checkbox
checked={inputs.PasswordLoginEnabled === 'true'}
label="允许密码登录"
name="PasswordLoginEnabled"
label='允许通过密码进行登录'
name='PasswordLoginEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.PasswordRegisterEnabled === 'true'}
label="允许通过密码进行注册"
name="PasswordRegisterEnabled"
label='允许通过密码进行注册'
name='PasswordRegisterEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.EmailVerificationEnabled === 'true'}
label="强制邮箱验证"
name="EmailVerificationEnabled"
label='通过密码注册时需要进行邮箱验证'
name='EmailVerificationEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.GitHubOAuthEnabled === 'true'}
label="允许通过 GitHub 账户登录 & 注册"
name="GitHubOAuthEnabled"
label='允许通过 GitHub 账户登录 & 注册'
name='GitHubOAuthEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.WeChatAuthEnabled === 'true'}
label="允许通过微信登录 & 注册"
name="WeChatAuthEnabled"
label='允许通过微信登录 & 注册'
name='WeChatAuthEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group inline>
<Form.Checkbox
checked={inputs.RegisterEnabled === 'true'}
label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
name='RegisterEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.TurnstileCheckEnabled === 'true'}
label='启用 Turnstile 用户校验'
name='TurnstileCheckEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Divider />
<Header as='h3'>
配置 SMTP
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Input
label="SMTP 服务器地址"
name="SMTPServer"
label='SMTP 服务器地址'
name='SMTPServer'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.SMTPServer}
placeholder='例如:smtp.qq.com'
/>
<Form.Input
label="SMTP 账户"
name="SMTPAccount"
label='SMTP 账户'
name='SMTPAccount'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.SMTPAccount}
placeholder='通常是邮箱地址'
/>
<Form.Input
label="SMTP 访问凭证"
name="SMTPToken"
label='SMTP 访问凭证'
name='SMTPToken'
onChange={handleInputChange}
type="password"
autoComplete="off"
type='password'
autoComplete='off'
value={inputs.SMTPToken}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
<Divider />
<Header as='h3'>
配置 GitHub OAuth App
<Header.Subheader>
用以支持通过 GitHub 进行登录注册
<a href='https://github.com/settings/developers' target='_blank'>
点击此处
</a>
管理你的 GitHub OAuth App
</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Input
label="GitHub Client ID"
name="GitHubClientId"
label='GitHub Client ID'
name='GitHubClientId'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.GitHubClientId}
placeholder='输入你注册的 GitHub OAuth APP 的 ID'
/>
<Form.Input
label="GitHub Client Secret"
name="GitHubClientSecret"
label='GitHub Client Secret'
name='GitHubClientSecret'
onChange={handleInputChange}
type="password"
autoComplete="off"
type='password'
autoComplete='off'
value={inputs.GitHubClientSecret}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitGitHubOAuth}>
保存 GitHub OAuth 设置
</Form.Button>
<Divider />
<Header as='h3'>
配置 WeChat Server
<Header.Subheader>
用以支持通过微信进行登录注册
<a
href='https://github.com/songquanpeng/wechat-server'
target='_blank'
>
点击此处
</a>
了解 WeChat Server
</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Input
label="WeChat Server 服务器地址"
name="WeChatServerAddress"
placeholder="例如:https://yourdomain.com(注意没有最后的斜杠)"
label='WeChat Server 服务器地址'
name='WeChatServerAddress'
placeholder='例如:https://yourdomain.com(注意没有最后的斜杠)'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.WeChatServerAddress}
/>
<Form.Input
label="WeChat Server 访问凭证"
name="WeChatServerToken"
type="password"
label='WeChat Server 访问凭证'
name='WeChatServerToken'
type='password'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.WeChatServerToken}
placeholder='敏感信息不会发送到前端显示'
/>
<Form.Input
label="微信公众号二维码图片链接"
name="WeChatAccountQRCodeImageURL"
label='微信公众号二维码图片链接'
name='WeChatAccountQRCodeImageURL'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.WeChatAccountQRCodeImageURL}
placeholder='输入一个图片链接'
/>
</Form.Group>
<Form.Button onClick={submitWeChat}>保存微信登录设置</Form.Button>
<Form.Button onClick={submitWeChat}>
保存 WeChat Server 设置
</Form.Button>
<Divider />
<Header as='h3'>
配置 Turnstile
<Header.Subheader>
用以支持用户校验
<a href='https://dash.cloudflare.com/' target='_blank'>
点击此处
</a>
管理你的 Turnstile Sites推荐选择 Invisible Widget Type
</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Input
label='Turnstile Site Key'
name='TurnstileSiteKey'
onChange={handleInputChange}
autoComplete='off'
value={inputs.TurnstileSiteKey}
placeholder='输入你注册的 Turnstile Site Key'
/>
<Form.Input
label='Turnstile Secret Key'
name='TurnstileSecretKey'
onChange={handleInputChange}
type='password'
autoComplete='off'
value={inputs.TurnstileSecretKey}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitTurnstile}>
保存 Turnstile 设置
</Form.Button>
</Form>
</Grid.Column>
</Grid>
+73 -29
View File
@@ -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 <Label>普通用户</Label>;
case 10:
return <Label color="yellow">管理员</Label>;
return <Label color='yellow'>管理员</Label>;
case 100:
return <Label color="orange">超级管理员</Label>;
return <Label color='orange'>超级管理员</Label>;
default:
return <Label color="red">未知身份</Label>;
return <Label color='red'>未知身份</Label>;
}
}
@@ -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) {
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 }) => {
(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 <Label basic>已激活</Label>;
case 2:
return '已封禁';
return (
<Label basic color='red'>
已封禁
</Label>
);
default:
return '未知状态';
return (
<Label basic color='grey'>
未知状态
</Label>
);
}
};
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 = () => {
<>
<Form onSubmit={searchUsers}>
<Form.Input
icon="search"
icon='search'
fluid
iconPosition="left"
placeholder="搜索用户的 ID,用户名,显示名称,以及邮箱地址 ..."
iconPosition='left'
placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
@@ -168,22 +204,26 @@ const UsersTable = () => {
<Table.Body>
{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 (
<Table.Row key={user.id}>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{user.display_name}</Table.Cell>
<Table.Cell>{user.email ? user.email : '无'}</Table.Cell>
<Table.Cell>{renderRole(user.role)}</Table.Cell>
<Table.Cell>{renderStatus(user.status, user.id)}</Table.Cell>
<Table.Cell>{renderStatus(user.status)}</Table.Cell>
<Table.Cell>
<div>
<Button
size={'small'}
positive
onClick={() => {
manageUser(user.username, 'promote');
manageUser(user.username, 'promote', idx);
}}
>
提升
@@ -192,7 +232,7 @@ const UsersTable = () => {
size={'small'}
color={'yellow'}
onClick={() => {
manageUser(user.username, 'demote');
manageUser(user.username, 'demote', idx);
}}
>
降级
@@ -201,7 +241,7 @@ const UsersTable = () => {
size={'small'}
negative
onClick={() => {
manageUser(user.username, 'delete');
manageUser(user.username, 'delete', idx);
}}
>
删除
@@ -211,7 +251,8 @@ const UsersTable = () => {
onClick={() => {
manageUser(
user.username,
user.status === 1 ? 'disable' : 'enable'
user.status === 1 ? 'disable' : 'enable',
idx
);
}}
>
@@ -233,17 +274,20 @@ const UsersTable = () => {
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan="6">
<Button size="small" as={Link} to="/user/add" loading={loading}>
<Table.HeaderCell colSpan='6'>
<Button size='small' as={Link} to='/user/add' loading={loading}>
添加新的用户
</Button>
<Pagination
floated="right"
floated='right'
activePage={activePage}
onPageChange={onPaginationChange}
size="small"
size='small'
siblingRange={1}
totalPages={Math.ceil(users.length / itemsPerPage)}
totalPages={
Math.ceil(users.length / ITEMS_PER_PAGE) +
(users.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
}
/>
</Table.HeaderCell>
</Table.Row>
+1
View File
@@ -0,0 +1 @@
export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
+1
View File
@@ -1,2 +1,3 @@
export * from './toast.constants';
export * from './user.constants';
export * from './common.constant';
+4
View File
@@ -85,3 +85,7 @@ export function showInfo(message) {
export function showNotice(message) {
toast.info(message, showNoticeOptions);
}
export function openPage(url) {
window.open(url);
}
+8 -6
View File
@@ -26,18 +26,20 @@ const About = () => {
return (
<>
<Segment>
{
about === '' ? <>
{about === '' ? (
<>
<Header as='h3'>关于</Header>
<p>可在设置页面设置关于内容支持 HTML & Markdown</p>
项目仓库地址
<a href="https://github.com/songquanpeng/message-pusher">
<a href='https://github.com/songquanpeng/message-pusher'>
https://github.com/songquanpeng/message-pusher
</a>
</> : <>
<div dangerouslySetInnerHTML={{ __html: about}}></div>
</>
}
) : (
<>
<div dangerouslySetInnerHTML={{ __html: about }}></div>
</>
)}
</Segment>
</>
);
+22 -47
View File
@@ -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 (
<>
<Segment loading={loading}>
<Header as='h3'>更新用户信息</Header>
<Form autoComplete='off'>
<Header as="h3">更新用户信息</Header>
<Form autoComplete="off">
<Form.Field>
<Form.Input
label='用户名'
name='username'
label="用户名"
name="username"
placeholder={'请输入新的用户名'}
onChange={handleInputChange}
value={username}
autoComplete='off'
autoComplete="off"
/>
</Form.Field>
<Form.Field>
<Form.Input
label='密码'
name='password'
label="密码"
name="password"
type={'password'}
placeholder={'请输入新的密码'}
onChange={handleInputChange}
value={password}
autoComplete='off'
autoComplete="off"
/>
</Form.Field>
<Form.Field>
<Form.Input
label='显示名称'
name='display_name'
label="显示名称"
name="display_name"
placeholder={'请输入新的显示名称'}
onChange={handleInputChange}
value={display_name}
autoComplete='off'
autoComplete="off"
/>
</Form.Field>
<Form.Field>
<Form.Input
label='推送鉴权 Token'
name='token'
placeholder={'请输入新的 Token,留空则将 Token 置空'}
onChange={handleInputChange}
value={token}
autoComplete='off'
/>
</Form.Field>
<Form.Field>
<Form.Input
label='已绑定的 GitHub 账户'
name='github_id'
label="已绑定的 GitHub 账户"
name="github_id"
value={github_id}
autoComplete='off'
autoComplete="off"
readOnly
placeholder={'如需绑定请到个人设置页面进行绑定'}
/>
</Form.Field>
<Form.Field>
<Form.Input
label='已绑定的微信账户'
name='wechat_id'
label="已绑定的微信账户"
name="wechat_id"
value={wechat_id}
autoComplete='off'
autoComplete="off"
readOnly
placeholder={'如需绑定请到个人设置页面进行绑定'}
/>
</Form.Field>
<Form.Field>
<Form.Input
label='已绑定的邮箱账户'
name='email'
label="已绑定的邮箱账户"
name="email"
value={email}
autoComplete='off'
autoComplete="off"
readOnly
placeholder={'如需绑定请到个人设置页面进行绑定'}
/>
</Form.Field>
<Button onClick={submit}>提交</Button>