Move Muse mirror-user provisioning into new-api control plane
Muse needs a trusted path to create and disable per-user long-lived relay tokens so billing, routing, and downgrade logic can stay inside new-api instead of leaking back into Muse. This adds a small internal route group guarded by a shared secret, provisions a mirror user plus a dedicated long-lived token, and exposes disable/status/revoke operations for the control plane. The sync path stores the Muse user id in remark as a temporary lookup anchor until Muse persists the returned new-api ids. Constraint: Must reuse new-api billing/token model behavior without adding schema changes in phase 1 Constraint: Must avoid CreateUser side effects such as signup quota grants for mirrored internal users Rejected: Reusing existing self-service token controllers | they only manage the authenticated owner and cannot return control-plane ids/keys Rejected: Keying mirror users by username/email alone | unstable and collision-prone for external identity sync Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Treat remark-based muse_user_id lookup as a phase-1 bridge; prefer persisted new_api_user_id from Muse binding records for later operations Tested: go test ./controller ./service ./middleware -run MuseInternal -count=1 Tested: go test ./controller ./service ./middleware -count=1 Not-tested: Full relay/data-plane integration against a live Muse control-plane caller
This commit is contained in:
parent
13da2edb0a
commit
3f24ba87ee
67
controller/muse_internal.go
Normal file
67
controller/muse_internal.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
museDTO "github.com/QuantumNous/new-api/dto"
|
||||||
|
"github.com/QuantumNous/new-api/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SyncMuseUser(c *gin.Context) {
|
||||||
|
var req museDTO.SyncMuseUserRequest
|
||||||
|
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
|
||||||
|
common.ApiError(c, errors.New("invalid request body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.SyncMuseUser(req)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.ApiSuccess(c, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisableMuseUser(c *gin.Context) {
|
||||||
|
userID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, errors.New("invalid user id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.DisableMuseUserByID(userID)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.ApiSuccess(c, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RevokeMuseToken(c *gin.Context) {
|
||||||
|
tokenID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, errors.New("invalid token id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.RevokeMuseToken(tokenID)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.ApiSuccess(c, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMuseUserStatus(c *gin.Context) {
|
||||||
|
userID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, errors.New("invalid user id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response, err := service.GetMuseUserStatus(userID)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
common.ApiSuccess(c, response)
|
||||||
|
}
|
||||||
151
controller/muse_internal_test.go
Normal file
151
controller/muse_internal_test.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
museDTO "github.com/QuantumNous/new-api/dto"
|
||||||
|
"github.com/QuantumNous/new-api/middleware"
|
||||||
|
"github.com/QuantumNous/new-api/model"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type museInternalAPIResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data museDTO.SyncMuseUserResponse `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMuseInternalControllerTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
common.UsingSQLite = true
|
||||||
|
common.UsingMySQL = false
|
||||||
|
common.UsingPostgreSQL = false
|
||||||
|
common.RedisEnabled = false
|
||||||
|
common.GlobalApiRateLimitEnable = false
|
||||||
|
|
||||||
|
t.Setenv("MUSE_INTERNAL_SECRET", "test-secret")
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
model.DB = db
|
||||||
|
model.LOG_DB = db
|
||||||
|
require.NoError(t, db.AutoMigrate(&model.User{}, &model.Token{}))
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err == nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeMuseInternalResponse(t *testing.T, recorder *httptest.ResponseRecorder) museInternalAPIResponse {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var response museInternalAPIResponse
|
||||||
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMuseInternalRouter() *gin.Engine {
|
||||||
|
engine := gin.New()
|
||||||
|
internal := engine.Group("/api/internal/muse")
|
||||||
|
internal.Use(middleware.MuseInternalAuth())
|
||||||
|
{
|
||||||
|
internal.POST("/users/sync", SyncMuseUser)
|
||||||
|
internal.POST("/users/:id/disable", DisableMuseUser)
|
||||||
|
internal.POST("/tokens/:id/revoke", RevokeMuseToken)
|
||||||
|
internal.GET("/users/:id/status", GetMuseUserStatus)
|
||||||
|
}
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuseInternalSyncUserCreatesMirrorUserAndToken(t *testing.T) {
|
||||||
|
setupMuseInternalControllerTestDB(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/internal/muse/users/sync", strings.NewReader(`{"muse_user_id":"u-1","username":"alice","display_name":"Alice","status":"active"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Muse-Service-Secret", "test-secret")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
newMuseInternalRouter().ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
response := decodeMuseInternalResponse(t, rr)
|
||||||
|
require.True(t, response.Success)
|
||||||
|
require.Equal(t, "active", response.Data.Status)
|
||||||
|
require.NotZero(t, response.Data.NewAPIUserID)
|
||||||
|
require.NotZero(t, response.Data.TokenID)
|
||||||
|
require.NotEmpty(t, response.Data.TokenKey)
|
||||||
|
|
||||||
|
user, err := model.GetUserById(response.Data.NewAPIUserID, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, common.UserStatusEnabled, user.Status)
|
||||||
|
require.Equal(t, "alice", user.Username)
|
||||||
|
require.Equal(t, "muse_user_id:u-1", user.Remark)
|
||||||
|
|
||||||
|
token, err := model.GetTokenById(response.Data.TokenID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, user.Id, token.UserId)
|
||||||
|
require.Equal(t, common.TokenStatusEnabled, token.Status)
|
||||||
|
require.True(t, token.UnlimitedQuota)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuseInternalSyncUserRejectsInvalidSecret(t *testing.T) {
|
||||||
|
setupMuseInternalControllerTestDB(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/internal/muse/users/sync", strings.NewReader(`{"muse_user_id":"u-1","username":"alice","display_name":"Alice","status":"active"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Muse-Service-Secret", "wrong-secret")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
newMuseInternalRouter().ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMuseInternalDisableUserDisablesUserAndToken(t *testing.T) {
|
||||||
|
setupMuseInternalControllerTestDB(t)
|
||||||
|
|
||||||
|
syncReq := httptest.NewRequest(http.MethodPost, "/api/internal/muse/users/sync", strings.NewReader(`{"muse_user_id":"u-2","username":"bob","display_name":"Bob","status":"active"}`))
|
||||||
|
syncReq.Header.Set("Content-Type", "application/json")
|
||||||
|
syncReq.Header.Set("X-Muse-Service-Secret", "test-secret")
|
||||||
|
syncRR := httptest.NewRecorder()
|
||||||
|
engine := newMuseInternalRouter()
|
||||||
|
engine.ServeHTTP(syncRR, syncReq)
|
||||||
|
|
||||||
|
syncResponse := decodeMuseInternalResponse(t, syncRR)
|
||||||
|
require.True(t, syncResponse.Success)
|
||||||
|
|
||||||
|
disableReq := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/internal/muse/users/%d/disable", syncResponse.Data.NewAPIUserID), nil)
|
||||||
|
disableReq.Header.Set("X-Muse-Service-Secret", "test-secret")
|
||||||
|
disableRR := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(disableRR, disableReq)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, disableRR.Code)
|
||||||
|
disableResponse := decodeMuseInternalResponse(t, disableRR)
|
||||||
|
require.True(t, disableResponse.Success)
|
||||||
|
require.Equal(t, "disabled", disableResponse.Data.Status)
|
||||||
|
require.Equal(t, syncResponse.Data.NewAPIUserID, disableResponse.Data.NewAPIUserID)
|
||||||
|
|
||||||
|
user, err := model.GetUserById(syncResponse.Data.NewAPIUserID, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, common.UserStatusDisabled, user.Status)
|
||||||
|
|
||||||
|
token, err := model.GetTokenById(syncResponse.Data.TokenID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, common.TokenStatusDisabled, token.Status)
|
||||||
|
}
|
||||||
15
dto/muse_internal.go
Normal file
15
dto/muse_internal.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type SyncMuseUserRequest struct {
|
||||||
|
MuseUserID string `json:"muse_user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncMuseUserResponse struct {
|
||||||
|
NewAPIUserID int `json:"newapi_user_id"`
|
||||||
|
TokenID int `json:"token_id"`
|
||||||
|
TokenKey string `json:"token_key,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
25
middleware/muse_internal_auth.go
Normal file
25
middleware/muse_internal_auth.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MuseInternalAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
secret := c.GetHeader("X-Muse-Service-Secret")
|
||||||
|
expected := common.GetEnvOrDefaultString("MUSE_INTERNAL_SECRET", "")
|
||||||
|
if expected == "" || subtle.ConstantTimeCompare([]byte(secret), []byte(expected)) != 1 {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "invalid muse internal secret",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -53,6 +53,15 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
// Universal secure verification routes
|
// Universal secure verification routes
|
||||||
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
|
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
|
||||||
|
|
||||||
|
museInternalRoute := apiRouter.Group("/internal/muse")
|
||||||
|
museInternalRoute.Use(middleware.MuseInternalAuth())
|
||||||
|
{
|
||||||
|
museInternalRoute.POST("/users/sync", controller.SyncMuseUser)
|
||||||
|
museInternalRoute.POST("/users/:id/disable", controller.DisableMuseUser)
|
||||||
|
museInternalRoute.POST("/tokens/:id/revoke", controller.RevokeMuseToken)
|
||||||
|
museInternalRoute.GET("/users/:id/status", controller.GetMuseUserStatus)
|
||||||
|
}
|
||||||
|
|
||||||
userRoute := apiRouter.Group("/user")
|
userRoute := apiRouter.Group("/user")
|
||||||
{
|
{
|
||||||
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
||||||
|
|||||||
305
service/muse_internal_service.go
Normal file
305
service/muse_internal_service.go
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
museDTO "github.com/QuantumNous/new-api/dto"
|
||||||
|
"github.com/QuantumNous/new-api/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
museUserStatusActive = "active"
|
||||||
|
museUserStatusDisabled = "disabled"
|
||||||
|
museMirrorTokenName = "muse-long-lived"
|
||||||
|
museRemarkPrefix = "muse_user_id:"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SyncMuseUser(req museDTO.SyncMuseUserRequest) (*museDTO.SyncMuseUserResponse, error) {
|
||||||
|
req.MuseUserID = strings.TrimSpace(req.MuseUserID)
|
||||||
|
req.Username = strings.TrimSpace(req.Username)
|
||||||
|
req.DisplayName = strings.TrimSpace(req.DisplayName)
|
||||||
|
if req.MuseUserID == "" {
|
||||||
|
return nil, errors.New("muse_user_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if normalizeMuseStatus(req.Status) == museUserStatusDisabled {
|
||||||
|
return disableMuseUserByMuseUserID(req.MuseUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := getMirrorUserByMuseUserID(req.MuseUserID)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user, err = createMirrorUser(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err = updateMirrorUser(user, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := ensureMirrorToken(user.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buildMirrorResponse(user, token, museUserStatusActive, true), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisableMuseUserByID(newAPIUserID int) (*museDTO.SyncMuseUserResponse, error) {
|
||||||
|
if newAPIUserID <= 0 {
|
||||||
|
return nil, errors.New("newapi user id is required")
|
||||||
|
}
|
||||||
|
user, err := model.GetUserById(newAPIUserID, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = disableUserAndTokens(user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
token, _ := getMirrorToken(user.Id)
|
||||||
|
return buildMirrorResponse(user, token, museUserStatusDisabled, false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RevokeMuseToken(tokenID int) (*museDTO.SyncMuseUserResponse, error) {
|
||||||
|
if tokenID <= 0 {
|
||||||
|
return nil, errors.New("token id is required")
|
||||||
|
}
|
||||||
|
token, err := model.GetTokenById(tokenID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
token.Status = common.TokenStatusDisabled
|
||||||
|
if err = token.Update(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user, err := model.GetUserById(token.UserId, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buildMirrorResponse(user, token, museUserStatusDisabled, false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMuseUserStatus(newAPIUserID int) (*museDTO.SyncMuseUserResponse, error) {
|
||||||
|
if newAPIUserID <= 0 {
|
||||||
|
return nil, errors.New("newapi user id is required")
|
||||||
|
}
|
||||||
|
user, err := model.GetUserById(newAPIUserID, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
token, _ := getMirrorToken(user.Id)
|
||||||
|
return buildMirrorResponse(user, token, userStatusToMuseStatus(user.Status), false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableMuseUserByMuseUserID(museUserID string) (*museDTO.SyncMuseUserResponse, error) {
|
||||||
|
user, err := getMirrorUserByMuseUserID(museUserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = disableUserAndTokens(user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
token, _ := getMirrorToken(user.Id)
|
||||||
|
return buildMirrorResponse(user, token, museUserStatusDisabled, false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMirrorUserByMuseUserID(museUserID string) (*model.User, error) {
|
||||||
|
user := &model.User{}
|
||||||
|
err := model.DB.Where("remark = ?", museRemarkPrefix+museUserID).First(user).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMirrorUser(req museDTO.SyncMuseUserRequest) (*model.User, error) {
|
||||||
|
hashedPassword, err := common.Password2Hash(common.GetRandomString(20))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user := &model.User{
|
||||||
|
Username: chooseMirrorUsername(req.Username, req.MuseUserID, 0, ""),
|
||||||
|
Password: hashedPassword,
|
||||||
|
DisplayName: chooseDisplayName(req.DisplayName, req.Username),
|
||||||
|
Role: common.RoleCommonUser,
|
||||||
|
Status: common.UserStatusEnabled,
|
||||||
|
Group: "default",
|
||||||
|
Remark: museRemarkPrefix + req.MuseUserID,
|
||||||
|
}
|
||||||
|
if user.DisplayName == "" {
|
||||||
|
user.DisplayName = user.Username
|
||||||
|
}
|
||||||
|
if err = model.DB.Create(user).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMirrorUser(user *model.User, req museDTO.SyncMuseUserRequest) error {
|
||||||
|
username := chooseMirrorUsername(req.Username, req.MuseUserID, user.Id, user.Username)
|
||||||
|
displayName := chooseDisplayName(req.DisplayName, req.Username)
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = user.DisplayName
|
||||||
|
}
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = username
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Username = username
|
||||||
|
user.DisplayName = displayName
|
||||||
|
user.Status = common.UserStatusEnabled
|
||||||
|
user.Role = common.RoleCommonUser
|
||||||
|
user.Remark = museRemarkPrefix + req.MuseUserID
|
||||||
|
return user.Update(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureMirrorToken(userID int) (*model.Token, error) {
|
||||||
|
token, err := getMirrorToken(userID)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
key, keyErr := common.GenerateKey()
|
||||||
|
if keyErr != nil {
|
||||||
|
return nil, keyErr
|
||||||
|
}
|
||||||
|
token = &model.Token{
|
||||||
|
UserId: userID,
|
||||||
|
Name: museMirrorTokenName,
|
||||||
|
Key: key,
|
||||||
|
Status: common.TokenStatusEnabled,
|
||||||
|
CreatedTime: common.GetTimestamp(),
|
||||||
|
AccessedTime: common.GetTimestamp(),
|
||||||
|
ExpiredTime: -1,
|
||||||
|
RemainQuota: 0,
|
||||||
|
UnlimitedQuota: true,
|
||||||
|
}
|
||||||
|
if err = token.Insert(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token.Status = common.TokenStatusEnabled
|
||||||
|
token.ExpiredTime = -1
|
||||||
|
token.UnlimitedQuota = true
|
||||||
|
token.RemainQuota = 0
|
||||||
|
token.AccessedTime = common.GetTimestamp()
|
||||||
|
if err = token.Update(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMirrorToken(userID int) (*model.Token, error) {
|
||||||
|
token := &model.Token{}
|
||||||
|
err := model.DB.Where("user_id = ? AND name = ?", userID, museMirrorTokenName).Order("id desc").First(token).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableUserAndTokens(user *model.User) error {
|
||||||
|
user.Status = common.UserStatusDisabled
|
||||||
|
if err := user.Update(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := make([]model.Token, 0)
|
||||||
|
if err := model.DB.Where("user_id = ?", user.Id).Find(&tokens).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range tokens {
|
||||||
|
tokens[i].Status = common.TokenStatusDisabled
|
||||||
|
if err := tokens[i].Update(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMirrorResponse(user *model.User, token *model.Token, status string, includeTokenKey bool) *museDTO.SyncMuseUserResponse {
|
||||||
|
response := &museDTO.SyncMuseUserResponse{Status: status}
|
||||||
|
if user != nil {
|
||||||
|
response.NewAPIUserID = user.Id
|
||||||
|
}
|
||||||
|
if token != nil {
|
||||||
|
response.TokenID = token.Id
|
||||||
|
if includeTokenKey {
|
||||||
|
response.TokenKey = token.GetFullKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeMuseStatus(status string) string {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(status), museUserStatusDisabled) {
|
||||||
|
return museUserStatusDisabled
|
||||||
|
}
|
||||||
|
return museUserStatusActive
|
||||||
|
}
|
||||||
|
|
||||||
|
func chooseDisplayName(displayName string, username string) string {
|
||||||
|
if strings.TrimSpace(displayName) != "" {
|
||||||
|
return trimToLength(displayName, model.UserNameMaxLength)
|
||||||
|
}
|
||||||
|
return trimToLength(username, model.UserNameMaxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chooseMirrorUsername(desired string, museUserID string, currentUserID int, currentUsername string) string {
|
||||||
|
desired = trimToLength(desired, model.UserNameMaxLength)
|
||||||
|
if desired != "" && usernameAvailable(desired, currentUserID) {
|
||||||
|
return desired
|
||||||
|
}
|
||||||
|
currentUsername = trimToLength(currentUsername, model.UserNameMaxLength)
|
||||||
|
if currentUsername != "" && usernameAvailable(currentUsername, currentUserID) {
|
||||||
|
return currentUsername
|
||||||
|
}
|
||||||
|
fallback := trimToLength(fmt.Sprintf("muse_%s", common.GenerateHMAC(museUserID)[:15]), model.UserNameMaxLength)
|
||||||
|
if usernameAvailable(fallback, currentUserID) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
candidate := trimToLength("m"+common.GetRandomString(model.UserNameMaxLength-1), model.UserNameMaxLength)
|
||||||
|
if usernameAvailable(candidate, currentUserID) {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func usernameAvailable(username string, currentUserID int) bool {
|
||||||
|
if username == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
query := model.DB.Model(&model.User{}).Where("username = ?", username)
|
||||||
|
if currentUserID > 0 {
|
||||||
|
query = query.Where("id <> ?", currentUserID)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := query.Count(&count).Error; err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return count == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimToLength(value string, limit int) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if len(value) <= limit {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
func userStatusToMuseStatus(status int) string {
|
||||||
|
if status == common.UserStatusDisabled {
|
||||||
|
return museUserStatusDisabled
|
||||||
|
}
|
||||||
|
return museUserStatusActive
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user