feat: 完善用户治理与登录白名单

This commit is contained in:
richjohnrambo
2026-09-08 10:55:26 +08:00
parent d239bd9ad0
commit 8116fb6fe8
8 changed files with 196 additions and 8 deletions
+83 -2
View File
@@ -18,6 +18,7 @@ import (
"sync"
"time"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
@@ -79,7 +80,7 @@ func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
}
}()
if req.Method != http.MethodGet {
if req.Method != http.MethodGet && req.Method != http.MethodPost {
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
return
}
@@ -97,6 +98,10 @@ func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix
if !ok {
return
}
if req.Method == http.MethodPost && !(len(parts) == 3 && parts[2] == "users") {
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
return
}
switch {
case len(parts) == 4 && parts[2] == "dashboard" && parts[3] == "overview":
@@ -107,7 +112,9 @@ func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix
serveInternalAdminResourceList(wrt, req, tenant.ID, "private-messages", "")
case len(parts) == 3 && parts[2] == "group-messages":
serveInternalAdminResourceList(wrt, req, tenant.ID, "group-messages", "")
case len(parts) == 3 && parts[2] == "users":
case req.Method == http.MethodPost && len(parts) == 3 && parts[2] == "users":
serveInternalUserCreate(wrt, req, tenant.ID)
case req.Method == http.MethodGet && len(parts) == 3 && parts[2] == "users":
serveInternalAdminResourceList(wrt, req, tenant.ID, "users", "")
case len(parts) == 4 && parts[2] == "users":
serveInternalUserQuery(wrt, req, tenant.ID, parts[3])
@@ -156,6 +163,80 @@ func serveInternalAdminQuery(wrt http.ResponseWriter, req *http.Request, prefix
}
}
type internalUserCreateRequest struct {
Username string `json:"username"`
Nickname string `json:"nickname"`
Phone string `json:"phone"`
Password string `json:"password"`
}
func serveInternalUserCreate(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID) {
var body internalUserCreateRequest
if err := json.NewDecoder(io.LimitReader(req.Body, 1<<20)).Decode(&body); err != nil {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
return
}
body.Username = strings.ToLower(strings.TrimSpace(body.Username))
body.Nickname = strings.TrimSpace(body.Nickname)
body.Phone = strings.TrimSpace(body.Phone)
if body.Username == "" {
var err error
body.Username, err = randomRegistrationUsername()
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
}
if err := validateRegistrationUsername(body.Username); err != nil || len([]rune(body.Password)) < 3 || len([]byte(body.Password)) > 72 {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "用户名或密码格式不正确", nil)
return
}
if body.Phone != "" {
phone, err := normalizeIMPhone(body.Phone, "CN")
if err != nil {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "手机号格式不正确", nil)
return
}
body.Phone = phone
}
authHandler := store.Store.GetLogicalAuthHandler("basic")
if authHandler == nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "basic authentication unavailable", nil)
return
}
authCtx := auth.AuthContext{TenantID: tenantID, RemoteAddr: req.RemoteAddr}
secret := []byte(body.Username + ":" + body.Password)
if unique, err := authHandler.IsUnique(authCtx, secret); err != nil || !unique {
writeIMError(wrt, req, http.StatusConflict, 30001, "DUPLICATE_USER", "用户名已存在", nil)
return
}
user := &types.User{
TenantID: tenantID,
State: types.StateOK,
Username: body.Username,
Phone: body.Phone,
Public: map[string]any{"fn": firstNonEmpty(body.Nickname, body.Username)},
}
user.Access.Auth = getDefaultAccess(types.TopicCatP2P, true, false) | getDefaultAccess(types.TopicCatGrp, true, false)
user.Access.Anon = getDefaultAccess(types.TopicCatP2P, false, false) | getDefaultAccess(types.TopicCatGrp, false, false)
created, err := store.Users.Create(tenantID, user, nil)
if err != nil {
writeInternalQueryError(wrt, req, err)
return
}
if _, err = authHandler.AddRecord(authCtx, &auth.Rec{TenantID: tenantID, Uid: created.Uid()}, secret); err != nil {
_ = store.Users.Delete(tenantID, created.Uid(), true)
writeInternalQueryError(wrt, req, err)
return
}
writeIMSuccess(wrt, req, map[string]any{
"user_id": store.DecodeUid(created.Uid()),
"username": created.Username,
"nickname": body.Nickname,
"phone": created.Phone,
})
}
func authorizeInternalAPI(wrt http.ResponseWriter, req *http.Request, capability string) bool {
if !internalAPI.config.Enabled {
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonMalformedRequest, "resource not found", nil)