Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ COPY --from=builder $SERVER_DIR/start-config.yml $SERVER_DIR/
COPY --from=builder $SERVER_DIR/go.mod $SERVER_DIR/
COPY --from=builder $SERVER_DIR/go.sum $SERVER_DIR/

RUN go get github.com/openimsdk/gomake@v0.0.14-alpha.5
# 使用与 go.mod 一致的 gomake 版本(勿降级)
RUN go mod download github.com/openimsdk/gomake

# Set the command to run when the container starts
ENTRYPOINT ["sh", "-c", "mage start && tail -f /dev/null"]
35 changes: 29 additions & 6 deletions internal/api/admin/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
disetcd "github.com/openimsdk/chat/pkg/common/kdisc/etcd"
adminclient "github.com/openimsdk/chat/pkg/protocol/admin"
chatclient "github.com/openimsdk/chat/pkg/protocol/chat"
"github.com/openimsdk/tools/db/mongoutil"
"github.com/openimsdk/tools/discovery"
"github.com/openimsdk/tools/discovery/etcd"
"github.com/openimsdk/tools/errs"
Expand Down Expand Up @@ -71,10 +72,21 @@ func Start(ctx context.Context, index int, config *Config) error {
}
adminApi := New(chatClient, adminClient, im, &base)
mwApi := chatmw.New(adminClient)

// 二开:初始化 MongoDB(用于白名单管理直接操作)
mgocli, err := mongoutil.NewMongoDB(ctx, config.AllConfig.Mongo.Build())
if err != nil {
return err
}
whitelistMgr, err := NewWhitelistManager(mgocli.GetDB())
if err != nil {
return err
}

gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(gin.Recovery(), mw.CorsHandler(), mw.GinParseOperationID())
SetAdminRoute(engine, adminApi, mwApi, config, client)
engine.Use(gin.Recovery(), mw.CorsHandler(), mw.GinParseOperationID(), chatmw.RateLimitByIP)
SetAdminRoute(engine, adminApi, mwApi, whitelistMgr, config, client)

if config.Discovery.Enable == kdisc.ETCDCONST {
cm := disetcd.NewConfigManager(client.(*etcd.SvcDiscoveryRegistryImpl).GetClient(), config.GetConfigNames())
Expand Down Expand Up @@ -118,8 +130,7 @@ func Start(ctx context.Context, index int, config *Config) error {
return nil
}

func SetAdminRoute(router gin.IRouter, admin *Api, mw *chatmw.MW, cfg *Config, client discovery.SvcDiscoveryRegistry) {

func SetAdminRoute(router gin.IRouter, admin *Api, mw *chatmw.MW, wlMgr *WhitelistManager, cfg *Config, client discovery.SvcDiscoveryRegistry) {
adminRouterGroup := router.Group("/account")
adminRouterGroup.POST("/login", admin.AdminLogin) // Login
adminRouterGroup.POST("/update", mw.CheckAdmin, admin.AdminUpdateInfo) // Modify information
Expand All @@ -129,7 +140,9 @@ func SetAdminRoute(router gin.IRouter, admin *Api, mw *chatmw.MW, cfg *Config, c
adminRouterGroup.POST("/add_user", mw.CheckAdmin, admin.AddUserAccount) // Add user account
adminRouterGroup.POST("/del_admin", mw.CheckAdmin, admin.DelAdminAccount) // Delete admin
adminRouterGroup.POST("/search", mw.CheckAdmin, admin.SearchAdminAccount) // Get admin list
//account.POST("/add_notification_account")
// account.POST("/add_notification_account")

router.POST("/user/batch_register", mw.CheckAdmin, admin.BatchRegisterUsers)

importGroup := router.Group("/user/import")
importGroup.POST("/json", mw.CheckAdmin, admin.ImportUserByJson)
Expand Down Expand Up @@ -180,7 +193,17 @@ func SetAdminRoute(router gin.IRouter, admin *Api, mw *chatmw.MW, cfg *Config, c
blockRouter.POST("/search", admin.SearchBlockUser) // Search blocked users

userRouter := router.Group("/user", mw.CheckAdmin)
userRouter.POST("/password/reset", admin.ResetUserPassword) // Reset user password
userRouter.POST("/password/reset", admin.ResetUserPassword) // Reset user password
userRouter.POST("/set_app_role", admin.SetAppRole) // 二开:设置用户端管理员
userRouter.POST("/ip_logs", admin.GetUserIPLogs) // 二开:查询用户 IP 登录历史
userRouter.POST("/search", admin.SearchUserInfo) // 二开:搜索用户(含 IP/角色,供管理端列表)

// 二开:白名单管理(仅超级管理员)
whitelistRouter := router.Group("/whitelist", mw.CheckAdmin)
whitelistRouter.POST("/add", wlMgr.AddWhitelist)
whitelistRouter.POST("/del", wlMgr.DelWhitelist)
whitelistRouter.POST("/update", wlMgr.UpdateWhitelist)
whitelistRouter.POST("/search", wlMgr.SearchWhitelist)

initGroup := router.Group("/client_config", mw.CheckAdmin)
initGroup.POST("/get", admin.GetClientConfig) // Get client initialization configuration
Expand Down
164 changes: 164 additions & 0 deletions internal/api/admin/whitelist_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// 二开:白名单管理 HTTP 处理器(admin API 层直接访问 MongoDB)
package admin

import (
"time"

"github.com/gin-gonic/gin"
adminmodel "github.com/openimsdk/chat/pkg/common/db/model/admin"
admindb "github.com/openimsdk/chat/pkg/common/db/table/admin"
"github.com/google/uuid"
"github.com/openimsdk/tools/apiresp"
"github.com/openimsdk/tools/errs"
"go.mongodb.org/mongo-driver/mongo"
)

// simplePage implements pagination.Pagination for whitelist searches
type simplePage struct {
pageNum int32
showNum int32
}

func (p *simplePage) GetPageNumber() int32 { return p.pageNum }
func (p *simplePage) GetShowNumber() int32 { return p.showNum }

// WhitelistManager 管理白名单的 HTTP 处理器
type WhitelistManager struct {
db admindb.WhitelistInterface
}

func NewWhitelistManager(mongoDB *mongo.Database) (*WhitelistManager, error) {
wl, err := adminmodel.NewWhitelistUser(mongoDB)
if err != nil {
return nil, err
}
return &WhitelistManager{db: wl}, nil
}

// AddWhitelistReq 添加白名单请求
type AddWhitelistReq struct {
Identifier string `json:"identifier" binding:"required"` // +8613800138000 or email
Type int32 `json:"type" binding:"required"` // 1=phone 2=email
Role string `json:"role"` // admin/operator/user
Permissions []string `json:"permissions"` // view_ip/ban_user/view_chat_log/broadcast
Remark string `json:"remark"`
}

// UpdateWhitelistReq 修改白名单请求
type UpdateWhitelistReq struct {
ID string `json:"id" binding:"required"`
Role *string `json:"role"`
Permissions []string `json:"permissions"`
Status *int32 `json:"status"` // 0=禁用 1=启用
Remark *string `json:"remark"`
}

// DelWhitelistReq 删除白名单请求
type DelWhitelistReq struct {
IDs []string `json:"ids" binding:"required"`
}

// SearchWhitelistReq 搜索白名单请求
type SearchWhitelistReq struct {
Keyword string `json:"keyword"`
Status int32 `json:"status"` // -1=全部 0=禁用 1=启用
PageNum int32 `json:"pageNum"` // 1-based
ShowNum int32 `json:"showNum"`
}

// AddWhitelist POST /whitelist/add
func (m *WhitelistManager) AddWhitelist(c *gin.Context) {
var req AddWhitelistReq
if err := c.ShouldBindJSON(&req); err != nil {
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
return
}
if req.Role == "" {
req.Role = "user"
}
now := time.Now()
entry := &admindb.WhitelistUser{
ID: uuid.New().String(),
Identifier: req.Identifier,
Type: req.Type,
Role: req.Role,
Permissions: req.Permissions,
Status: admindb.WhitelistStatusActive,
Remark: req.Remark,
CreateTime: now,
UpdateTime: now,
}
if err := m.db.Create(c, []*admindb.WhitelistUser{entry}); err != nil {
apiresp.GinError(c, err)
return
}
apiresp.GinSuccess(c, entry)
}

// UpdateWhitelist POST /whitelist/update
func (m *WhitelistManager) UpdateWhitelist(c *gin.Context) {
var req UpdateWhitelistReq
if err := c.ShouldBindJSON(&req); err != nil {
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
return
}
update := map[string]any{"update_time": time.Now()}
if req.Role != nil {
update["role"] = *req.Role
}
if req.Permissions != nil {
update["permissions"] = req.Permissions
}
if req.Status != nil {
update["status"] = *req.Status
}
if req.Remark != nil {
update["remark"] = *req.Remark
}
if err := m.db.Update(c, req.ID, update); err != nil {
apiresp.GinError(c, err)
return
}
apiresp.GinSuccess(c, nil)
}

// DelWhitelist POST /whitelist/del
func (m *WhitelistManager) DelWhitelist(c *gin.Context) {
var req DelWhitelistReq
if err := c.ShouldBindJSON(&req); err != nil {
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
return
}
if err := m.db.Delete(c, req.IDs); err != nil {
apiresp.GinError(c, err)
return
}
apiresp.GinSuccess(c, nil)
}

// SearchWhitelist POST /whitelist/search
func (m *WhitelistManager) SearchWhitelist(c *gin.Context) {
var req SearchWhitelistReq
if err := c.ShouldBindJSON(&req); err != nil {
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
return
}
if req.ShowNum <= 0 {
req.ShowNum = 20
}
if req.PageNum <= 0 {
req.PageNum = 1
}
total, list, err := m.db.Search(c, req.Keyword, req.Status, &simplePage{
pageNum: req.PageNum,
showNum: req.ShowNum,
})
if err != nil {
apiresp.GinError(c, err)
return
}
apiresp.GinSuccess(c, map[string]any{
"total": total,
"list": list,
})
}
27 changes: 27 additions & 0 deletions internal/api/chat/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import (
"time"

"github.com/openimsdk/chat/internal/api/util"
chatmw "github.com/openimsdk/chat/internal/api/mw"

"github.com/gin-gonic/gin"
"github.com/openimsdk/chat/pkg/common/apistruct"
"github.com/openimsdk/chat/pkg/common/imapi"
"github.com/openimsdk/chat/pkg/common/mctx"
"github.com/openimsdk/chat/pkg/eerrs"
"github.com/openimsdk/chat/pkg/protocol/admin"
chatpb "github.com/openimsdk/chat/pkg/protocol/chat"
constantpb "github.com/openimsdk/protocol/constant"
Expand Down Expand Up @@ -168,11 +170,30 @@ func (o *Api) Login(c *gin.Context) {
return
}
req.Ip = ip
// 二开:检查登录锁定(5 次失败后锁定 5 分钟)
lockKey := req.PhoneNumber
if req.Account != "" {
lockKey = req.Account
} else if req.Email != "" {
lockKey = req.Email
}
if lockKey != "" && chatmw.IsLoginLocked(lockKey) {
apiresp.GinError(c, eerrs.ErrForbidden.WrapMsg("登录失败次数过多,账号已锁定5分钟"))
return
}
resp, err := o.chatClient.Login(c, req)
if err != nil {
// 记录失败次数(密码错误或账号不存在时)
if lockKey != "" {
chatmw.RecordLoginFailure(lockKey)
}
apiresp.GinError(c, err)
return
}
// 登录成功,重置失败计数
if lockKey != "" {
chatmw.ResetLoginFailure(lockKey)
}
adminToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c)
if err != nil {
apiresp.GinError(c, err)
Expand All @@ -189,6 +210,7 @@ func (o *Api) Login(c *gin.Context) {
ImToken: imToken,
UserID: resp.UserID,
ChatToken: resp.ChatToken,
AppRole: resp.AppRole,
})
}

Expand Down Expand Up @@ -283,6 +305,11 @@ func (o *Api) GetTokenForVideoMeeting(c *gin.Context) {
a2r.Call(c, chatpb.ChatClient.GetTokenForVideoMeeting, o.chatClient)
}

// 二开:查询指定用户 IP(仅管理员或用户端管理员可调)
func (o *Api) GetUserIPInfo(c *gin.Context) {
a2r.Call(c, chatpb.ChatClient.GetUserIPInfo, o.chatClient)
}

// ################## APPLET ##################

func (o *Api) FindApplet(c *gin.Context) {
Expand Down
5 changes: 3 additions & 2 deletions internal/api/chat/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func Start(ctx context.Context, index int, cfg *Config) error {
mwApi := chatmw.New(adminClient)
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(gin.Recovery(), mw.CorsHandler(), mw.GinParseOperationID())
engine.Use(gin.Recovery(), mw.CorsHandler(), mw.GinParseOperationID(), chatmw.RateLimitByIP)
SetChatRoute(engine, adminApi, mwApi)

var (
Expand Down Expand Up @@ -140,7 +140,8 @@ func SetChatRoute(router gin.IRouter, chat *Api, mw *chatmw.MW) {
user.POST("/find/full", chat.FindUserFullInfo) // Get all information of the user
user.POST("/search/full", chat.SearchUserFullInfo) // Search user's public information
user.POST("/search/public", chat.SearchUserPublicInfo) // Search all information of the user
user.POST("/rtc/get_token", chat.GetTokenForVideoMeeting) // Get token for video meeting for the user
user.POST("/rtc/get_token", chat.GetTokenForVideoMeeting) // Get token for video meeting for the user
user.POST("/ip_info", chat.GetUserIPInfo) // 二开:查询指定用户 IP(需管理员或用户端管理员权限)

router.POST("/friend/search", mw.CheckToken, chat.SearchFriend)

Expand Down
Loading
Loading