diff --git a/deployments/helm-charts/config.yaml b/deployments/helm-charts/config.yaml index 9b4c569e9..537f373e3 100644 --- a/deployments/helm-charts/config.yaml +++ b/deployments/helm-charts/config.yaml @@ -85,6 +85,8 @@ config: withStack: false secret: openIM123 + chatSecret: openIM123 + tokenPolicy: expire: 86400 diff --git a/deployments/templates/config.yaml b/deployments/templates/config.yaml index ccf6dfcd5..56d105292 100644 --- a/deployments/templates/config.yaml +++ b/deployments/templates/config.yaml @@ -72,7 +72,8 @@ log: withStack: false # Whether to include stack trace in logs # Secret key for secure communication -secret: openIM123 # SECRET, Secret key for encryption and secure communication +secret: openIM123 # SECRET, Secret OpenIM key for encryption and secure communication +chatSecret: openIM123 # ChatSecret, ChatSecret chat key for encryption and secure communication # Token policy configuration tokenPolicy: diff --git a/go.mod b/go.mod index 71ae189ec..99a5860eb 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/OpenIMSDK/tools v0.0.17 github.com/go-zookeeper/zk v1.0.3 github.com/redis/go-redis/v9 v9.1.0 + github.com/xuri/excelize/v2 v2.8.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) @@ -66,27 +67,32 @@ require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect github.com/tjfoc/gmsm v1.3.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca // indirect + github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect go.mongodb.org/mongo-driver v1.12.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/image v0.9.0 // indirect - golang.org/x/net v0.12.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/image v0.11.0 // indirect + golang.org/x/net v0.14.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 8d33b2e79..d44b7cedc 100644 --- a/go.sum +++ b/go.sum @@ -137,6 +137,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= @@ -151,6 +153,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= +github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -181,6 +188,12 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg= +github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.8.0 h1:Vd4Qy809fupgp1v7X+nCS/MioeQmYVVzi495UCTqB7U= +github.com/xuri/excelize/v2 v2.8.0/go.mod h1:6iA2edBTKxKbZAa7X5bDhcCg51xdOn1Ar5sfoXRGrQg= +github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a h1:Mw2VNrNNNjDtw68VsEj2+st+oCSn4Uz7vZw6TbhcV1o= +github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -208,10 +221,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g= -golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -228,8 +241,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -253,13 +266,14 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= @@ -269,8 +283,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/internal/api/admin.go b/internal/api/admin.go index 3499691ae..1d860bf80 100644 --- a/internal/api/admin.go +++ b/internal/api/admin.go @@ -15,10 +15,17 @@ package api import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" "github.com/OpenIMSDK/chat/pkg/common/apicall" "github.com/OpenIMSDK/chat/pkg/common/apistruct" "github.com/OpenIMSDK/chat/pkg/common/config" + constant2 "github.com/OpenIMSDK/chat/pkg/common/constant" "github.com/OpenIMSDK/chat/pkg/common/mctx" + "github.com/OpenIMSDK/chat/pkg/common/xlsx" + "github.com/OpenIMSDK/chat/pkg/common/xlsx/model" "github.com/OpenIMSDK/chat/pkg/proto/admin" "github.com/OpenIMSDK/chat/pkg/proto/chat" "github.com/OpenIMSDK/protocol/constant" @@ -32,6 +39,11 @@ import ( "github.com/OpenIMSDK/tools/utils" "github.com/gin-gonic/gin" "google.golang.org/grpc" + "net" + "net/http" + "strconv" + "strings" + "time" ) func NewAdmin(chatConn, adminConn grpc.ClientConnInterface) *AdminApi { @@ -387,3 +399,203 @@ func (o *AdminApi) SearchLogs(c *gin.Context) { func (o *AdminApi) DeleteLogs(c *gin.Context) { a2r.Call(chat.ChatClient.DeleteLogs, o.chatClient, c) } + +func (o *AdminApi) getClientIP(c *gin.Context) (string, error) { + if config.Config.ProxyHeader == "" { + ip, _, err := net.SplitHostPort(c.Request.RemoteAddr) + return ip, err + } + ip := c.Request.Header.Get(config.Config.ProxyHeader) + if ip == "" { + return "", errs.ErrInternalServer.Wrap() + } + if ip := net.ParseIP(ip); ip == nil { + return "", errs.ErrInternalServer.Wrap(fmt.Sprintf("parse proxy ip header %s failed", ip)) + } + return ip, nil +} + +func (o *AdminApi) checkSecretAdmin(c *gin.Context, secret string) error { + if _, ok := c.Get(constant2.RpcOpUserID); ok { + return nil + } + if config.Config.ChatSecret == "" { + return errs.ErrNoPermission.Wrap("not config chat secret") + } + if config.Config.ChatSecret != secret { + return errs.ErrNoPermission.Wrap("secret error") + } + SetToken(c, config.GetDefaultIMAdmin(), constant2.AdminUser) + return nil +} + +func (o *AdminApi) ImportUserByXlsx(c *gin.Context) { + defer log.ZDebug(c, "ImportUserByXlsx return") + formFile, err := c.FormFile("data") + if err != nil { + apiresp.GinError(c, err) + return + } + ip, err := o.getClientIP(c) + if err != nil { + apiresp.GinError(c, err) + return + } + secret := c.PostForm("secret") + if err := o.checkSecretAdmin(c, secret); err != nil { + apiresp.GinError(c, err) + return + } + file, err := formFile.Open() + if err != nil { + apiresp.GinError(c, err) + return + } + defer file.Close() + var users []model.User + if err := xlsx.ParseAll(file, &users); err != nil { + apiresp.GinError(c, errs.ErrArgs.Wrap("xlsx file parse error "+err.Error())) + return + } + us, err := o.xlsx2user(users) + if err != nil { + apiresp.GinError(c, err) + return + } + imToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c) + if err != nil { + apiresp.GinError(c, err) + return + } + + ctx := mctx.WithAdminUser(mctx.WithApiToken(c, imToken)) + apiresp.GinError(c, o.registerChatUser(ctx, ip, us)) +} + +func (o *AdminApi) ImportUserByJson(c *gin.Context) { + var req struct { + Secret string `json:"secret"` + Users []*chat.RegisterUserInfo `json:"users"` + } + if err := c.BindJSON(&req); err != nil { + apiresp.GinError(c, err) + return + } + ip, err := o.getClientIP(c) + if err != nil { + apiresp.GinError(c, err) + return + } + if err := o.checkSecretAdmin(c, req.Secret); err != nil { + apiresp.GinError(c, err) + return + } + imToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c) + if err != nil { + apiresp.GinError(c, err) + return + } + ctx := mctx.WithAdminUser(mctx.WithApiToken(c, imToken)) + apiresp.GinError(c, o.registerChatUser(ctx, ip, req.Users)) +} + +func (o *AdminApi) xlsx2user(users []model.User) ([]*chat.RegisterUserInfo, error) { + chatUsers := make([]*chat.RegisterUserInfo, len(users)) + for i, info := range users { + if info.Nickname == "" { + return nil, errs.ErrArgs.Wrap("nickname is empty") + } + if info.AreaCode == "" || info.PhoneNumber == "" { + return nil, errs.ErrArgs.Wrap("areaCode or phoneNumber is empty") + } + if info.Password == "" { + return nil, errs.ErrArgs.Wrap("password is empty") + } + if !strings.HasPrefix(info.AreaCode, "+") { + return nil, errs.ErrArgs.Wrap("areaCode format error") + } + if _, err := strconv.ParseUint(info.AreaCode[1:], 10, 16); err != nil { + return nil, errs.ErrArgs.Wrap("areaCode format error") + } + gender, _ := strconv.Atoi(info.Gender) + chatUsers[i] = &chat.RegisterUserInfo{ + UserID: info.UserID, + Nickname: info.Nickname, + FaceURL: info.FaceURL, + Birth: o.xlsxBirth(info.Birth).UnixMilli(), + Gender: int32(gender), + AreaCode: info.AreaCode, + PhoneNumber: info.PhoneNumber, + Email: info.Email, + Account: info.Account, + Password: utils.Md5(info.Password), + } + } + return chatUsers, nil +} + +func (o *AdminApi) xlsxBirth(s string) time.Time { + if s == "" { + return time.Now() + } + var separator byte + for _, b := range []byte(s) { + if b < '0' || b > '9' { + separator = b + } + } + arr := strings.Split(s, string([]byte{separator})) + if len(arr) != 3 { + return time.Now() + } + year, _ := strconv.Atoi(arr[0]) + month, _ := strconv.Atoi(arr[1]) + day, _ := strconv.Atoi(arr[2]) + t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local) + if t.Before(time.Date(1900, 0, 0, 0, 0, 0, 0, time.Local)) { + return time.Now() + } + return t +} + +func (o *AdminApi) registerChatUser(ctx context.Context, ip string, users []*chat.RegisterUserInfo) error { + if len(users) == 0 { + return errs.ErrArgs.Wrap("users is empty") + } + for _, info := range users { + respRegisterUser, err := o.chatClient.RegisterUser(ctx, &chat.RegisterUserReq{Ip: ip, User: info, Platform: constant.AdminPlatformID}) + if err != nil { + return err + } + userInfo := &sdkws.UserInfo{ + UserID: respRegisterUser.UserID, + Nickname: info.Nickname, + FaceURL: info.FaceURL, + } + if err = o.imApiCaller.RegisterUser(ctx, []*sdkws.UserInfo{userInfo}); err != nil { + return err + } + if resp, err := o.adminClient.FindDefaultFriend(ctx, &admin.FindDefaultFriendReq{}); err == nil { + _ = o.imApiCaller.ImportFriend(ctx, respRegisterUser.UserID, resp.UserIDs) + } + if resp, err := o.adminClient.FindDefaultGroup(ctx, &admin.FindDefaultGroupReq{}); err == nil { + _ = o.imApiCaller.InviteToGroup(ctx, respRegisterUser.UserID, resp.GroupIDs) + } + } + return nil +} + +func (o *AdminApi) BatchImportTemplate(c *gin.Context) { + md5Sum := md5.Sum(config.ImportTemplate) + md5Val := hex.EncodeToString(md5Sum[:]) + if c.GetHeader("If-None-Match") == md5Val { + c.Status(http.StatusNotModified) + return + } + c.Header("Content-Disposition", "attachment; filename=template.xlsx") + c.Header("Content-Transfer-Encoding", "binary") + c.Header("Content-Description", "File Transfer") + c.Header("Content-Length", strconv.Itoa(len(config.ImportTemplate))) + c.Header("ETag", md5Val) + c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", config.ImportTemplate) +} diff --git a/internal/api/mw.go b/internal/api/mw.go index d3d1a4328..755600363 100644 --- a/internal/api/mw.go +++ b/internal/api/mw.go @@ -87,9 +87,7 @@ func (o *MW) isValidToken(c *gin.Context, userID string, token string) error { } func (o *MW) setToken(c *gin.Context, userID string, userType int32) { - c.Set(constant.RpcOpUserID, userID) - c.Set(constant.RpcOpUserType, []string{strconv.Itoa(int(userType))}) - c.Set(constant.RpcCustomHeader, []string{constant.RpcOpUserType}) + SetToken(c, userID, userType) } func (o *MW) CheckToken(c *gin.Context) { @@ -147,3 +145,9 @@ func (o *MW) CheckAdminOrNil(c *gin.Context) { o.setToken(c, userID, constant.AdminUser) } } + +func SetToken(c *gin.Context, userID string, userType int32) { + c.Set(constant.RpcOpUserID, userID) + c.Set(constant.RpcOpUserType, []string{strconv.Itoa(int(userType))}) + c.Set(constant.RpcCustomHeader, []string{constant.RpcOpUserType}) +} diff --git a/internal/api/router.go b/internal/api/router.go index 723bcc9c4..aa6581624 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -81,6 +81,11 @@ func NewAdminRoute(router gin.IRouter, discov discoveryregistry.SvcDiscoveryRegi adminRouterGroup.POST("/del_admin", mw.CheckAdmin, admin.DelAdminAccount) // Delete admin adminRouterGroup.POST("/search", mw.CheckAdmin, admin.SearchAdminAccount) // Get admin list + importGroup := router.Group("/user/import") + importGroup.POST("/json", mw.CheckAdminOrNil, admin.ImportUserByJson) + importGroup.POST("/xlsx", mw.CheckAdminOrNil, admin.ImportUserByXlsx) + importGroup.GET("/xlsx", admin.BatchImportTemplate) + defaultRouter := router.Group("/default", mw.CheckAdmin) defaultUserRouter := defaultRouter.Group("/user") defaultUserRouter.POST("/add", admin.AddDefaultFriend) // Add default friend at registration diff --git a/pkg/common/config/config.go b/pkg/common/config/config.go index b8579b2af..c75817589 100644 --- a/pkg/common/config/config.go +++ b/pkg/common/config/config.go @@ -16,8 +16,12 @@ package config import _ "embed" -//go:embed version -var Version string +var ( + //go:embed version + Version string + //go:embed template.xlsx + ImportTemplate []byte +) var Config struct { Envs struct { @@ -75,6 +79,7 @@ var Config struct { WithStack *bool `yaml:"withStack"` } `yaml:"log"` Secret *string `yaml:"secret"` + ChatSecret string `yaml:"chatSecret"` OpenIMUrl string `yaml:"openIMUrl"` TokenPolicy struct { Expire *int64 `yaml:"expire"` diff --git a/pkg/common/config/template.xlsx b/pkg/common/config/template.xlsx new file mode 100644 index 000000000..7f4007899 Binary files /dev/null and b/pkg/common/config/template.xlsx differ diff --git a/pkg/common/xlsx/main.go b/pkg/common/xlsx/main.go new file mode 100644 index 000000000..c170bd195 --- /dev/null +++ b/pkg/common/xlsx/main.go @@ -0,0 +1,115 @@ +package xlsx + +import ( + "errors" + "github.com/xuri/excelize/v2" + "io" + "reflect" +) + +func ParseSheet(file *excelize.File, v interface{}) error { + val := reflect.ValueOf(v) + if val.Kind() != reflect.Ptr { + return errors.New("not ptr") + } + val = val.Elem() + if val.Kind() != reflect.Slice { + return errors.New("not slice") + } + itemType := val.Type().Elem() + if itemType.Kind() != reflect.Struct { + return errors.New("not struct") + } + newItemValue := func() reflect.Value { + return reflect.New(itemType).Elem() + } + putItem := func(v reflect.Value) { + val.Set(reflect.Append(val, v)) + } + var sheetName string + if s, ok := newItemValue().Interface().(SheetName); ok { + sheetName = s.SheetName() + } else { + sheetName = itemType.Name() + } + + if sheetIndex, err := file.GetSheetIndex(sheetName); err != nil { + return err + } else if sheetIndex < 0 { + return nil + } + fieldIndex := make(map[string]int) // 结构体对应的下标 + for i := 0; i < itemType.NumField(); i++ { + field := itemType.Field(i) + alias := field.Tag.Get("column") + switch alias { + case "": + fieldIndex[field.Name] = i + case "-": + continue + default: + fieldIndex[alias] = i + } + } + if len(fieldIndex) == 0 { + return errors.New("empty column struct") + } + sheetIndex := make(map[string]int) // sheet 对应的下标 + for i := 1; ; i++ { // 第一行 + name, err := file.GetCellValue(sheetName, GetAxis(i, 1)) + if err != nil { + return err + } + if name == "" { + break + } + if _, ok := fieldIndex[name]; ok { + sheetIndex[name] = i + } + } + if len(sheetIndex) == 0 { + return errors.New("sheet column empty") + } + for i := 2; ; i++ { + var ( + notEmpty int + item = newItemValue() + ) + for column, index := range sheetIndex { + s, err := file.GetCellValue(sheetName, GetAxis(index, i)) + if err != nil { + return err + } + if s == "" { + continue + } + notEmpty++ + if err = String2Value(s, item.Field(fieldIndex[column])); err != nil { + return err + } + } + if notEmpty > 0 { // 空行表示结束 + putItem(item) + } else { + break + } + } + return nil +} + +func ParseAll(r io.Reader, models ...interface{}) error { + if len(models) == 0 { + return errors.New("empty models") + } + file, err := excelize.OpenReader(r) + if err != nil { + return err + } + defer file.Close() + for i := 0; i < len(models); i++ { + if err := ParseSheet(file, models[i]); err != nil { + return err + } + } + return nil +} diff --git a/pkg/common/xlsx/model/user.go b/pkg/common/xlsx/model/user.go new file mode 100644 index 000000000..ada74bfa4 --- /dev/null +++ b/pkg/common/xlsx/model/user.go @@ -0,0 +1,18 @@ +package model + +type User struct { + UserID string `column:"user_id"` + Nickname string `column:"nickname"` + FaceURL string `column:"face_url"` + Birth string `column:"birth"` + Gender string `column:"gender"` + AreaCode string `column:"area_code"` + PhoneNumber string `column:"phone_number"` + Email string `column:"email"` + Account string `column:"account"` + Password string `column:"password"` +} + +func (User) SheetName() string { + return "user" +} diff --git a/pkg/common/xlsx/sheet.go b/pkg/common/xlsx/sheet.go new file mode 100644 index 000000000..f5a90d6e1 --- /dev/null +++ b/pkg/common/xlsx/sheet.go @@ -0,0 +1,5 @@ +package xlsx + +type SheetName interface { + SheetName() string +} diff --git a/pkg/common/xlsx/utils.go b/pkg/common/xlsx/utils.go new file mode 100644 index 000000000..885bb9625 --- /dev/null +++ b/pkg/common/xlsx/utils.go @@ -0,0 +1,200 @@ +package xlsx + +import ( + "errors" + "fmt" + "github.com/xuri/excelize/v2" + "io" + "reflect" + "strconv" + "strings" +) + +func Open(r io.Reader) (*excelize.File, error) { + return excelize.OpenReader(r) +} + +func GetAxis(x, y int) string { + return Num2AZ(x) + strconv.Itoa(y) +} + +func Num2AZ(num int) string { + var ( + str string + k int + temp []int //保存转化后每一位数据的值,然后通过索引的方式匹配A-Z + ) + //用来匹配的字符A-Z + slices := []string{"", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"} + + if num > 26 { //数据大于26需要进行拆分 + for { + k = num % 26 //从个位开始拆分,如果求余为0,说明末尾为26,也就是Z,如果是转化为26进制数,则末尾是可以为0的,这里必须为A-Z中的一个 + if k == 0 { + temp = append(temp, 26) + k = 26 + } else { + temp = append(temp, k) + } + num = (num - k) / 26 //减去num最后一位数的值,因为已经记录在temp中 + if num <= 26 { //小于等于26直接进行匹配,不需要进行数据拆分 + temp = append(temp, num) + break + } + } + } else { + return slices[num] + } + for _, value := range temp { + str = slices[value] + str //因为数据切分后存储顺序是反的,所以str要放在后面 + } + return str +} + +func String2Value(s string, rv reflect.Value) error { + var ( + val interface{} + err error + ) + if s == "" { + val = reflect.Zero(rv.Type()).Interface() + } else { + switch rv.Kind() { + case reflect.Bool: + switch strings.ToLower(s) { + case "false", "f", "0": + val = false + case "true", "t", "1": + val = true + default: + return fmt.Errorf("parse %s to bool error", s) + } + case reflect.Int: + val, err = strconv.Atoi(s) + case reflect.Int8: + t, err := strconv.ParseInt(s, 10, 8) + if err != nil { + return err + } + val = int8(t) + case reflect.Int16: + t, err := strconv.ParseInt(s, 10, 16) + if err != nil { + return err + } + val = int16(t) + case reflect.Int32: + t, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return err + } + val = int32(t) + case reflect.Int64: + val, err = strconv.ParseInt(s, 10, 64) + case reflect.Uint: + t, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return err + } + val = uint(t) + case reflect.Uint8: + t, err := strconv.ParseUint(s, 10, 8) + if err != nil { + return err + } + val = uint8(t) + case reflect.Uint16: + t, err := strconv.ParseUint(s, 10, 16) + if err != nil { + return err + } + val = uint16(t) + case reflect.Uint32: + t, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return err + } + val = uint32(t) + case reflect.Uint64: + val, err = strconv.ParseUint(s, 10, 64) + case reflect.Float32: + t, err := strconv.ParseFloat(s, 32) + if err != nil { + return err + } + val = float32(t) + case reflect.Float64: + val, err = strconv.ParseFloat(s, 64) + case reflect.String: + val = s + default: + return errors.New("not Supported " + rv.Kind().String()) + } + } + if err != nil { + return err + } + rv.Set(reflect.ValueOf(val)) + return nil +} + +func ZeroValue(kind reflect.Kind) (interface{}, error) { + var v interface{} + switch kind { + case reflect.Bool: + v = false + case reflect.Int: + v = int(0) + case reflect.Int8: + v = int8(0) + case reflect.Int16: + v = int16(0) + case reflect.Int32: + v = int32(0) + case reflect.Int64: + v = int64(0) + case reflect.Uint: + v = uint(0) + case reflect.Uint8: + v = uint8(0) + case reflect.Uint16: + v = uint16(0) + case reflect.Uint32: + v = uint32(0) + case reflect.Uint64: + v = uint64(0) + case reflect.Float32: + v = float32(0) + case reflect.Float64: + v = float64(0) + case reflect.String: + v = "" + default: + return nil, errors.New("not Supported " + kind.String()) + } + return v, nil +} + +func GetSheetName(v interface{}) string { + return getSheetName(reflect.TypeOf(v)) +} + +func getSheetName(t reflect.Type) string { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() == reflect.Slice { + t = t.Elem() + } + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return "" + } + if s, ok := reflect.New(t).Interface().(SheetName); ok { + return s.SheetName() + } else { + return t.Name() + } +} diff --git a/pkg/proto/chat/chat.go b/pkg/proto/chat/chat.go index a37d06a60..c7c4dd192 100644 --- a/pkg/proto/chat/chat.go +++ b/pkg/proto/chat/chat.go @@ -112,9 +112,9 @@ func (x *VerifyCodeReq) Check() error { } func (x *RegisterUserReq) Check() error { - if x.VerifyCode == "" { - return errs.ErrArgs.Wrap("VerifyCode is empty") - } + //if x.VerifyCode == "" { + // return errs.ErrArgs.Wrap("VerifyCode is empty") + //} if x.Platform < constant2.IOSPlatformID || x.Platform > constant2.AdminPlatformID { return errs.ErrArgs.Wrap("platform is invalid") }