Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
35291484
Unverified
Commit
35291484
authored
Mar 20, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 20, 2026
Browse files
Merge pull request #1151 from DaydreamCoding/feat/admin-user-group-filter
feat(admin): 用户管理新增分组列、分组筛选与专属分组一键替换
parents
01d8286b
ba7d2aec
Changes
29
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/admin_service_stub_test.go
View file @
35291484
...
@@ -445,5 +445,9 @@ func (s *stubAdminService) EnsureOpenAIPrivacy(ctx context.Context, account *ser
...
@@ -445,5 +445,9 @@ func (s *stubAdminService) EnsureOpenAIPrivacy(ctx context.Context, account *ser
return
""
return
""
}
}
func
(
s
*
stubAdminService
)
ReplaceUserGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
*
service
.
ReplaceUserGroupResult
,
error
)
{
return
&
service
.
ReplaceUserGroupResult
{
MigratedKeys
:
0
},
nil
}
// Ensure stub implements interface.
// Ensure stub implements interface.
var
_
service
.
AdminService
=
(
*
stubAdminService
)(
nil
)
var
_
service
.
AdminService
=
(
*
stubAdminService
)(
nil
)
backend/internal/handler/admin/user_handler.go
View file @
35291484
...
@@ -75,6 +75,7 @@ type UpdateBalanceRequest struct {
...
@@ -75,6 +75,7 @@ type UpdateBalanceRequest struct {
// - role: filter by user role
// - role: filter by user role
// - search: search in email, username
// - search: search in email, username
// - attr[{id}]: filter by custom attribute value, e.g. attr[1]=company
// - attr[{id}]: filter by custom attribute value, e.g. attr[1]=company
// - group_name: fuzzy filter by allowed group name
func
(
h
*
UserHandler
)
List
(
c
*
gin
.
Context
)
{
func
(
h
*
UserHandler
)
List
(
c
*
gin
.
Context
)
{
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
...
@@ -89,6 +90,7 @@ func (h *UserHandler) List(c *gin.Context) {
...
@@ -89,6 +90,7 @@ func (h *UserHandler) List(c *gin.Context) {
Status
:
c
.
Query
(
"status"
),
Status
:
c
.
Query
(
"status"
),
Role
:
c
.
Query
(
"role"
),
Role
:
c
.
Query
(
"role"
),
Search
:
search
,
Search
:
search
,
GroupName
:
strings
.
TrimSpace
(
c
.
Query
(
"group_name"
)),
Attributes
:
parseAttributeFilters
(
c
),
Attributes
:
parseAttributeFilters
(
c
),
}
}
if
raw
,
ok
:=
c
.
GetQuery
(
"include_subscriptions"
);
ok
{
if
raw
,
ok
:=
c
.
GetQuery
(
"include_subscriptions"
);
ok
{
...
@@ -366,3 +368,35 @@ func (h *UserHandler) GetBalanceHistory(c *gin.Context) {
...
@@ -366,3 +368,35 @@ func (h *UserHandler) GetBalanceHistory(c *gin.Context) {
"total_recharged"
:
totalRecharged
,
"total_recharged"
:
totalRecharged
,
})
})
}
}
// ReplaceGroupRequest represents the request to replace a user's exclusive group
type
ReplaceGroupRequest
struct
{
OldGroupID
int64
`json:"old_group_id" binding:"required,gt=0"`
NewGroupID
int64
`json:"new_group_id" binding:"required,gt=0"`
}
// ReplaceGroup handles replacing a user's exclusive group
// POST /api/v1/admin/users/:id/replace-group
func
(
h
*
UserHandler
)
ReplaceGroup
(
c
*
gin
.
Context
)
{
userID
,
err
:=
strconv
.
ParseInt
(
c
.
Param
(
"id"
),
10
,
64
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid user ID"
)
return
}
var
req
ReplaceGroupRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
result
,
err
:=
h
.
adminService
.
ReplaceUserGroup
(
c
.
Request
.
Context
(),
userID
,
req
.
OldGroupID
,
req
.
NewGroupID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"migrated_keys"
:
result
.
MigratedKeys
,
})
}
backend/internal/handler/sora_client_handler_test.go
View file @
35291484
...
@@ -942,6 +942,9 @@ func (r *stubUserRepoForHandler) ExistsByEmail(context.Context, string) (bool, e
...
@@ -942,6 +942,9 @@ func (r *stubUserRepoForHandler) ExistsByEmail(context.Context, string) (bool, e
func
(
r
*
stubUserRepoForHandler
)
RemoveGroupFromAllowedGroups
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
r
*
stubUserRepoForHandler
)
RemoveGroupFromAllowedGroups
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
return
0
,
nil
return
0
,
nil
}
}
func
(
r
*
stubUserRepoForHandler
)
RemoveGroupFromUserAllowedGroups
(
context
.
Context
,
int64
,
int64
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForHandler
)
UpdateTotpSecret
(
context
.
Context
,
int64
,
*
string
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForHandler
)
UpdateTotpSecret
(
context
.
Context
,
int64
,
*
string
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForHandler
)
EnableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForHandler
)
EnableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForHandler
)
DisableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForHandler
)
DisableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
...
@@ -1017,6 +1020,20 @@ func (r *stubAPIKeyRepoForHandler) SearchAPIKeys(context.Context, int64, string,
...
@@ -1017,6 +1020,20 @@ func (r *stubAPIKeyRepoForHandler) SearchAPIKeys(context.Context, int64, string,
func
(
r
*
stubAPIKeyRepoForHandler
)
ClearGroupIDByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
r
*
stubAPIKeyRepoForHandler
)
ClearGroupIDByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
return
0
,
nil
return
0
,
nil
}
}
func
(
r
*
stubAPIKeyRepoForHandler
)
UpdateGroupIDByUserAndGroup
(
_
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
int64
,
error
)
{
var
updated
int64
for
id
,
key
:=
range
r
.
keys
{
if
key
.
UserID
!=
userID
||
key
.
GroupID
==
nil
||
*
key
.
GroupID
!=
oldGroupID
{
continue
}
clone
:=
*
key
gid
:=
newGroupID
clone
.
GroupID
=
&
gid
r
.
keys
[
id
]
=
&
clone
updated
++
}
return
updated
,
nil
}
func
(
r
*
stubAPIKeyRepoForHandler
)
CountByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
r
*
stubAPIKeyRepoForHandler
)
CountByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
return
0
,
nil
return
0
,
nil
}
}
...
...
backend/internal/repository/api_key_repo.go
View file @
35291484
...
@@ -409,6 +409,16 @@ func (r *apiKeyRepository) ClearGroupIDByGroupID(ctx context.Context, groupID in
...
@@ -409,6 +409,16 @@ func (r *apiKeyRepository) ClearGroupIDByGroupID(ctx context.Context, groupID in
return
int64
(
n
),
err
return
int64
(
n
),
err
}
}
// UpdateGroupIDByUserAndGroup 将用户下绑定 oldGroupID 的所有 Key 迁移到 newGroupID
func
(
r
*
apiKeyRepository
)
UpdateGroupIDByUserAndGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
int64
,
error
)
{
client
:=
clientFromContext
(
ctx
,
r
.
client
)
n
,
err
:=
client
.
APIKey
.
Update
()
.
Where
(
apikey
.
UserIDEQ
(
userID
),
apikey
.
GroupIDEQ
(
oldGroupID
),
apikey
.
DeletedAtIsNil
())
.
SetGroupID
(
newGroupID
)
.
Save
(
ctx
)
return
int64
(
n
),
err
}
// CountByGroupID 获取分组的 API Key 数量
// CountByGroupID 获取分组的 API Key 数量
func
(
r
*
apiKeyRepository
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
func
(
r
*
apiKeyRepository
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
count
,
err
:=
r
.
activeQuery
()
.
Where
(
apikey
.
GroupIDEQ
(
groupID
))
.
Count
(
ctx
)
count
,
err
:=
r
.
activeQuery
()
.
Where
(
apikey
.
GroupIDEQ
(
groupID
))
.
Count
(
ctx
)
...
...
backend/internal/repository/user_repo.go
View file @
35291484
...
@@ -11,6 +11,7 @@ import (
...
@@ -11,6 +11,7 @@ import (
dbent
"github.com/Wei-Shaw/sub2api/ent"
dbent
"github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/apikey"
dbgroup
"github.com/Wei-Shaw/sub2api/ent/group"
dbuser
"github.com/Wei-Shaw/sub2api/ent/user"
dbuser
"github.com/Wei-Shaw/sub2api/ent/user"
"github.com/Wei-Shaw/sub2api/ent/userallowedgroup"
"github.com/Wei-Shaw/sub2api/ent/userallowedgroup"
"github.com/Wei-Shaw/sub2api/ent/usersubscription"
"github.com/Wei-Shaw/sub2api/ent/usersubscription"
...
@@ -200,6 +201,12 @@ func (r *userRepository) ListWithFilters(ctx context.Context, params pagination.
...
@@ -200,6 +201,12 @@ func (r *userRepository) ListWithFilters(ctx context.Context, params pagination.
)
)
}
}
if
filters
.
GroupName
!=
""
{
q
=
q
.
Where
(
dbuser
.
HasAllowedGroupsWith
(
dbgroup
.
NameContainsFold
(
filters
.
GroupName
),
))
}
// If attribute filters are specified, we need to filter by user IDs first
// If attribute filters are specified, we need to filter by user IDs first
var
allowedUserIDs
[]
int64
var
allowedUserIDs
[]
int64
if
len
(
filters
.
Attributes
)
>
0
{
if
len
(
filters
.
Attributes
)
>
0
{
...
@@ -453,6 +460,15 @@ func (r *userRepository) RemoveGroupFromAllowedGroups(ctx context.Context, group
...
@@ -453,6 +460,15 @@ func (r *userRepository) RemoveGroupFromAllowedGroups(ctx context.Context, group
return
int64
(
affected
),
nil
return
int64
(
affected
),
nil
}
}
// RemoveGroupFromUserAllowedGroups 移除单个用户的指定分组权限
func
(
r
*
userRepository
)
RemoveGroupFromUserAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
client
:=
clientFromContext
(
ctx
,
r
.
client
)
_
,
err
:=
client
.
UserAllowedGroup
.
Delete
()
.
Where
(
userallowedgroup
.
UserIDEQ
(
userID
),
userallowedgroup
.
GroupIDEQ
(
groupID
))
.
Exec
(
ctx
)
return
err
}
func
(
r
*
userRepository
)
GetFirstAdmin
(
ctx
context
.
Context
)
(
*
service
.
User
,
error
)
{
func
(
r
*
userRepository
)
GetFirstAdmin
(
ctx
context
.
Context
)
(
*
service
.
User
,
error
)
{
m
,
err
:=
r
.
client
.
User
.
Query
()
.
m
,
err
:=
r
.
client
.
User
.
Query
()
.
Where
(
Where
(
...
...
backend/internal/server/api_contract_test.go
View file @
35291484
...
@@ -807,6 +807,10 @@ func (r *stubUserRepo) RemoveGroupFromAllowedGroups(ctx context.Context, groupID
...
@@ -807,6 +807,10 @@ func (r *stubUserRepo) RemoveGroupFromAllowedGroups(ctx context.Context, groupID
return
0
,
errors
.
New
(
"not implemented"
)
return
0
,
errors
.
New
(
"not implemented"
)
}
}
func
(
r
*
stubUserRepo
)
RemoveGroupFromUserAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
return
errors
.
New
(
"not implemented"
)
}
func
(
r
*
stubUserRepo
)
AddGroupToAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
func
(
r
*
stubUserRepo
)
AddGroupToAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
return
errors
.
New
(
"not implemented"
)
return
errors
.
New
(
"not implemented"
)
}
}
...
@@ -1509,6 +1513,22 @@ func (r *stubApiKeyRepo) ClearGroupIDByGroupID(ctx context.Context, groupID int6
...
@@ -1509,6 +1513,22 @@ func (r *stubApiKeyRepo) ClearGroupIDByGroupID(ctx context.Context, groupID int6
return
0
,
errors
.
New
(
"not implemented"
)
return
0
,
errors
.
New
(
"not implemented"
)
}
}
func
(
r
*
stubApiKeyRepo
)
UpdateGroupIDByUserAndGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
int64
,
error
)
{
var
updated
int64
for
id
,
key
:=
range
r
.
byID
{
if
key
.
UserID
!=
userID
||
key
.
GroupID
==
nil
||
*
key
.
GroupID
!=
oldGroupID
{
continue
}
clone
:=
*
key
gid
:=
newGroupID
clone
.
GroupID
=
&
gid
r
.
byID
[
id
]
=
&
clone
r
.
byKey
[
clone
.
Key
]
=
&
clone
updated
++
}
return
updated
,
nil
}
func
(
r
*
stubApiKeyRepo
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
func
(
r
*
stubApiKeyRepo
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"not implemented"
)
return
0
,
errors
.
New
(
"not implemented"
)
}
}
...
...
backend/internal/server/middleware/admin_auth_test.go
View file @
35291484
...
@@ -181,6 +181,10 @@ func (s *stubUserRepo) RemoveGroupFromAllowedGroups(ctx context.Context, groupID
...
@@ -181,6 +181,10 @@ func (s *stubUserRepo) RemoveGroupFromAllowedGroups(ctx context.Context, groupID
panic
(
"unexpected RemoveGroupFromAllowedGroups call"
)
panic
(
"unexpected RemoveGroupFromAllowedGroups call"
)
}
}
func
(
s
*
stubUserRepo
)
RemoveGroupFromUserAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
panic
(
"unexpected RemoveGroupFromUserAllowedGroups call"
)
}
func
(
s
*
stubUserRepo
)
AddGroupToAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
func
(
s
*
stubUserRepo
)
AddGroupToAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
panic
(
"unexpected AddGroupToAllowedGroups call"
)
panic
(
"unexpected AddGroupToAllowedGroups call"
)
}
}
...
...
backend/internal/server/middleware/api_key_auth_google_test.go
View file @
35291484
...
@@ -104,6 +104,9 @@ func (f fakeAPIKeyRepo) ResetRateLimitWindows(ctx context.Context, id int64) err
...
@@ -104,6 +104,9 @@ func (f fakeAPIKeyRepo) ResetRateLimitWindows(ctx context.Context, id int64) err
func
(
f
fakeAPIKeyRepo
)
GetRateLimitData
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
APIKeyRateLimitData
,
error
)
{
func
(
f
fakeAPIKeyRepo
)
GetRateLimitData
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
APIKeyRateLimitData
,
error
)
{
return
&
service
.
APIKeyRateLimitData
{},
nil
return
&
service
.
APIKeyRateLimitData
{},
nil
}
}
func
(
f
fakeAPIKeyRepo
)
UpdateGroupIDByUserAndGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeGoogleSubscriptionRepo
)
Create
(
ctx
context
.
Context
,
sub
*
service
.
UserSubscription
)
error
{
func
(
f
fakeGoogleSubscriptionRepo
)
Create
(
ctx
context
.
Context
,
sub
*
service
.
UserSubscription
)
error
{
return
errors
.
New
(
"not implemented"
)
return
errors
.
New
(
"not implemented"
)
...
...
backend/internal/server/middleware/api_key_auth_test.go
View file @
35291484
...
@@ -565,6 +565,10 @@ func (r *stubApiKeyRepo) ClearGroupIDByGroupID(ctx context.Context, groupID int6
...
@@ -565,6 +565,10 @@ func (r *stubApiKeyRepo) ClearGroupIDByGroupID(ctx context.Context, groupID int6
return
0
,
errors
.
New
(
"not implemented"
)
return
0
,
errors
.
New
(
"not implemented"
)
}
}
func
(
r
*
stubApiKeyRepo
)
UpdateGroupIDByUserAndGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
r
*
stubApiKeyRepo
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
func
(
r
*
stubApiKeyRepo
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"not implemented"
)
return
0
,
errors
.
New
(
"not implemented"
)
}
}
...
...
backend/internal/server/routes/admin.go
View file @
35291484
...
@@ -215,6 +215,7 @@ func registerUserManagementRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
...
@@ -215,6 +215,7 @@ func registerUserManagementRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
users
.
GET
(
"/:id/api-keys"
,
h
.
Admin
.
User
.
GetUserAPIKeys
)
users
.
GET
(
"/:id/api-keys"
,
h
.
Admin
.
User
.
GetUserAPIKeys
)
users
.
GET
(
"/:id/usage"
,
h
.
Admin
.
User
.
GetUserUsage
)
users
.
GET
(
"/:id/usage"
,
h
.
Admin
.
User
.
GetUserUsage
)
users
.
GET
(
"/:id/balance-history"
,
h
.
Admin
.
User
.
GetBalanceHistory
)
users
.
GET
(
"/:id/balance-history"
,
h
.
Admin
.
User
.
GetBalanceHistory
)
users
.
POST
(
"/:id/replace-group"
,
h
.
Admin
.
User
.
ReplaceGroup
)
// User attribute values
// User attribute values
users
.
GET
(
"/:id/attributes"
,
h
.
Admin
.
UserAttribute
.
GetUserAttributes
)
users
.
GET
(
"/:id/attributes"
,
h
.
Admin
.
UserAttribute
.
GetUserAttributes
)
...
...
backend/internal/service/admin_service.go
View file @
35291484
...
@@ -50,6 +50,9 @@ type AdminService interface {
...
@@ -50,6 +50,9 @@ type AdminService interface {
// API Key management (admin)
// API Key management (admin)
AdminUpdateAPIKeyGroupID
(
ctx
context
.
Context
,
keyID
int64
,
groupID
*
int64
)
(
*
AdminUpdateAPIKeyGroupIDResult
,
error
)
AdminUpdateAPIKeyGroupID
(
ctx
context
.
Context
,
keyID
int64
,
groupID
*
int64
)
(
*
AdminUpdateAPIKeyGroupIDResult
,
error
)
// ReplaceUserGroup 替换用户的专属分组:授予新分组权限、迁移 Key、移除旧分组权限
ReplaceUserGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
*
ReplaceUserGroupResult
,
error
)
// Account management
// Account management
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
,
groupID
int64
)
([]
Account
,
int64
,
error
)
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
,
groupID
int64
)
([]
Account
,
int64
,
error
)
GetAccount
(
ctx
context
.
Context
,
id
int64
)
(
*
Account
,
error
)
GetAccount
(
ctx
context
.
Context
,
id
int64
)
(
*
Account
,
error
)
...
@@ -270,6 +273,11 @@ type AdminUpdateAPIKeyGroupIDResult struct {
...
@@ -270,6 +273,11 @@ type AdminUpdateAPIKeyGroupIDResult struct {
GrantedGroupName
string
// the group name that was auto-granted
GrantedGroupName
string
// the group name that was auto-granted
}
}
// ReplaceUserGroupResult 分组替换操作的结果
type
ReplaceUserGroupResult
struct
{
MigratedKeys
int64
// 迁移的 Key 数量
}
// BulkUpdateAccountsResult is the aggregated response for bulk updates.
// BulkUpdateAccountsResult is the aggregated response for bulk updates.
type
BulkUpdateAccountsResult
struct
{
type
BulkUpdateAccountsResult
struct
{
Success
int
`json:"success"`
Success
int
`json:"success"`
...
@@ -1377,6 +1385,71 @@ func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i
...
@@ -1377,6 +1385,71 @@ func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i
return
result
,
nil
return
result
,
nil
}
}
// ReplaceUserGroup 替换用户的专属分组
func
(
s
*
adminServiceImpl
)
ReplaceUserGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
*
ReplaceUserGroupResult
,
error
)
{
if
oldGroupID
==
newGroupID
{
return
nil
,
infraerrors
.
BadRequest
(
"SAME_GROUP"
,
"old and new group must be different"
)
}
// 验证新分组存在且为活跃的专属标准分组
newGroup
,
err
:=
s
.
groupRepo
.
GetByID
(
ctx
,
newGroupID
)
if
err
!=
nil
{
return
nil
,
err
}
if
newGroup
.
Status
!=
StatusActive
{
return
nil
,
infraerrors
.
BadRequest
(
"GROUP_NOT_ACTIVE"
,
"target group is not active"
)
}
if
!
newGroup
.
IsExclusive
{
return
nil
,
infraerrors
.
BadRequest
(
"GROUP_NOT_EXCLUSIVE"
,
"target group is not exclusive"
)
}
if
newGroup
.
IsSubscriptionType
()
{
return
nil
,
infraerrors
.
BadRequest
(
"GROUP_IS_SUBSCRIPTION"
,
"subscription groups are not supported for replacement"
)
}
// 事务保证原子性
if
s
.
entClient
==
nil
{
return
nil
,
fmt
.
Errorf
(
"entClient is nil, cannot perform group replacement"
)
}
tx
,
err
:=
s
.
entClient
.
Tx
(
ctx
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"begin transaction: %w"
,
err
)
}
defer
func
()
{
_
=
tx
.
Rollback
()
}()
opCtx
:=
dbent
.
NewTxContext
(
ctx
,
tx
)
// 1. 授予新分组权限
if
err
:=
s
.
userRepo
.
AddGroupToAllowedGroups
(
opCtx
,
userID
,
newGroupID
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"add new group to allowed groups: %w"
,
err
)
}
// 2. 迁移绑定旧分组的 Key 到新分组
migrated
,
err
:=
s
.
apiKeyRepo
.
UpdateGroupIDByUserAndGroup
(
opCtx
,
userID
,
oldGroupID
,
newGroupID
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"migrate api keys: %w"
,
err
)
}
// 3. 移除旧分组权限
if
err
:=
s
.
userRepo
.
RemoveGroupFromUserAllowedGroups
(
opCtx
,
userID
,
oldGroupID
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"remove old group from allowed groups: %w"
,
err
)
}
if
err
:=
tx
.
Commit
();
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"commit transaction: %w"
,
err
)
}
// 失效该用户所有 Key 的认证缓存
if
s
.
authCacheInvalidator
!=
nil
{
keys
,
keyErr
:=
s
.
apiKeyRepo
.
ListKeysByUserID
(
ctx
,
userID
)
if
keyErr
==
nil
{
for
_
,
k
:=
range
keys
{
s
.
authCacheInvalidator
.
InvalidateAuthCacheByKey
(
ctx
,
k
)
}
}
}
return
&
ReplaceUserGroupResult
{
MigratedKeys
:
migrated
},
nil
}
// Account management implementations
// Account management implementations
func
(
s
*
adminServiceImpl
)
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
,
groupID
int64
)
([]
Account
,
int64
,
error
)
{
func
(
s
*
adminServiceImpl
)
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
,
groupID
int64
)
([]
Account
,
int64
,
error
)
{
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
}
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
}
...
...
backend/internal/service/admin_service_apikey_test.go
View file @
35291484
...
@@ -65,6 +65,9 @@ func (s *userRepoStubForGroupUpdate) ExistsByEmail(context.Context, string) (boo
...
@@ -65,6 +65,9 @@ func (s *userRepoStubForGroupUpdate) ExistsByEmail(context.Context, string) (boo
func
(
s
*
userRepoStubForGroupUpdate
)
RemoveGroupFromAllowedGroups
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
s
*
userRepoStubForGroupUpdate
)
RemoveGroupFromAllowedGroups
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
panic
(
"unexpected"
)
panic
(
"unexpected"
)
}
}
func
(
s
*
userRepoStubForGroupUpdate
)
RemoveGroupFromUserAllowedGroups
(
context
.
Context
,
int64
,
int64
)
error
{
panic
(
"unexpected"
)
}
func
(
s
*
userRepoStubForGroupUpdate
)
UpdateTotpSecret
(
context
.
Context
,
int64
,
*
string
)
error
{
func
(
s
*
userRepoStubForGroupUpdate
)
UpdateTotpSecret
(
context
.
Context
,
int64
,
*
string
)
error
{
panic
(
"unexpected"
)
panic
(
"unexpected"
)
}
}
...
@@ -128,6 +131,9 @@ func (s *apiKeyRepoStubForGroupUpdate) SearchAPIKeys(context.Context, int64, str
...
@@ -128,6 +131,9 @@ func (s *apiKeyRepoStubForGroupUpdate) SearchAPIKeys(context.Context, int64, str
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
ClearGroupIDByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
ClearGroupIDByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
panic
(
"unexpected"
)
panic
(
"unexpected"
)
}
}
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
UpdateGroupIDByUserAndGroup
(
context
.
Context
,
int64
,
int64
,
int64
)
(
int64
,
error
)
{
panic
(
"unexpected"
)
}
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
CountByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
s
*
apiKeyRepoStubForGroupUpdate
)
CountByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
panic
(
"unexpected"
)
panic
(
"unexpected"
)
}
}
...
...
backend/internal/service/admin_service_delete_test.go
View file @
35291484
...
@@ -93,6 +93,10 @@ func (s *userRepoStub) RemoveGroupFromAllowedGroups(ctx context.Context, groupID
...
@@ -93,6 +93,10 @@ func (s *userRepoStub) RemoveGroupFromAllowedGroups(ctx context.Context, groupID
panic
(
"unexpected RemoveGroupFromAllowedGroups call"
)
panic
(
"unexpected RemoveGroupFromAllowedGroups call"
)
}
}
func
(
s
*
userRepoStub
)
RemoveGroupFromUserAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
panic
(
"unexpected RemoveGroupFromUserAllowedGroups call"
)
}
func
(
s
*
userRepoStub
)
AddGroupToAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
func
(
s
*
userRepoStub
)
AddGroupToAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
{
panic
(
"unexpected AddGroupToAllowedGroups call"
)
panic
(
"unexpected AddGroupToAllowedGroups call"
)
}
}
...
...
backend/internal/service/api_key_service.go
View file @
35291484
...
@@ -63,6 +63,8 @@ type APIKeyRepository interface {
...
@@ -63,6 +63,8 @@ type APIKeyRepository interface {
ListByGroupID
(
ctx
context
.
Context
,
groupID
int64
,
params
pagination
.
PaginationParams
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
ListByGroupID
(
ctx
context
.
Context
,
groupID
int64
,
params
pagination
.
PaginationParams
)
([]
APIKey
,
*
pagination
.
PaginationResult
,
error
)
SearchAPIKeys
(
ctx
context
.
Context
,
userID
int64
,
keyword
string
,
limit
int
)
([]
APIKey
,
error
)
SearchAPIKeys
(
ctx
context
.
Context
,
userID
int64
,
keyword
string
,
limit
int
)
([]
APIKey
,
error
)
ClearGroupIDByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
ClearGroupIDByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
// UpdateGroupIDByUserAndGroup 将用户下绑定 oldGroupID 的所有 Key 迁移到 newGroupID
UpdateGroupIDByUserAndGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
int64
,
error
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
ListKeysByUserID
(
ctx
context
.
Context
,
userID
int64
)
([]
string
,
error
)
ListKeysByUserID
(
ctx
context
.
Context
,
userID
int64
)
([]
string
,
error
)
ListKeysByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
([]
string
,
error
)
ListKeysByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
([]
string
,
error
)
...
...
backend/internal/service/api_key_service_cache_test.go
View file @
35291484
...
@@ -80,6 +80,9 @@ func (s *authRepoStub) SearchAPIKeys(ctx context.Context, userID int64, keyword
...
@@ -80,6 +80,9 @@ func (s *authRepoStub) SearchAPIKeys(ctx context.Context, userID int64, keyword
func
(
s
*
authRepoStub
)
ClearGroupIDByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
func
(
s
*
authRepoStub
)
ClearGroupIDByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected ClearGroupIDByGroupID call"
)
panic
(
"unexpected ClearGroupIDByGroupID call"
)
}
}
func
(
s
*
authRepoStub
)
UpdateGroupIDByUserAndGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected UpdateGroupIDByUserAndGroup call"
)
}
func
(
s
*
authRepoStub
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
func
(
s
*
authRepoStub
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected CountByGroupID call"
)
panic
(
"unexpected CountByGroupID call"
)
...
...
backend/internal/service/api_key_service_delete_test.go
View file @
35291484
...
@@ -108,6 +108,9 @@ func (s *apiKeyRepoStub) SearchAPIKeys(ctx context.Context, userID int64, keywor
...
@@ -108,6 +108,9 @@ func (s *apiKeyRepoStub) SearchAPIKeys(ctx context.Context, userID int64, keywor
func
(
s
*
apiKeyRepoStub
)
ClearGroupIDByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
func
(
s
*
apiKeyRepoStub
)
ClearGroupIDByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected ClearGroupIDByGroupID call"
)
panic
(
"unexpected ClearGroupIDByGroupID call"
)
}
}
func
(
s
*
apiKeyRepoStub
)
UpdateGroupIDByUserAndGroup
(
ctx
context
.
Context
,
userID
,
oldGroupID
,
newGroupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected UpdateGroupIDByUserAndGroup call"
)
}
func
(
s
*
apiKeyRepoStub
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
func
(
s
*
apiKeyRepoStub
)
CountByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected CountByGroupID call"
)
panic
(
"unexpected CountByGroupID call"
)
...
...
backend/internal/service/api_key_service_quota_test.go
View file @
35291484
...
@@ -122,6 +122,9 @@ func (s *quotaBaseAPIKeyRepoStub) SearchAPIKeys(context.Context, int64, string,
...
@@ -122,6 +122,9 @@ func (s *quotaBaseAPIKeyRepoStub) SearchAPIKeys(context.Context, int64, string,
func
(
s
*
quotaBaseAPIKeyRepoStub
)
ClearGroupIDByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
s
*
quotaBaseAPIKeyRepoStub
)
ClearGroupIDByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
panic
(
"unexpected ClearGroupIDByGroupID call"
)
panic
(
"unexpected ClearGroupIDByGroupID call"
)
}
}
func
(
s
*
quotaBaseAPIKeyRepoStub
)
UpdateGroupIDByUserAndGroup
(
context
.
Context
,
int64
,
int64
,
int64
)
(
int64
,
error
)
{
panic
(
"unexpected UpdateGroupIDByUserAndGroup call"
)
}
func
(
s
*
quotaBaseAPIKeyRepoStub
)
CountByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
s
*
quotaBaseAPIKeyRepoStub
)
CountByGroupID
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
panic
(
"unexpected CountByGroupID call"
)
panic
(
"unexpected CountByGroupID call"
)
}
}
...
...
backend/internal/service/sora_generation_service_test.go
View file @
35291484
...
@@ -162,6 +162,9 @@ func (r *stubUserRepoForQuota) ExistsByEmail(context.Context, string) (bool, err
...
@@ -162,6 +162,9 @@ func (r *stubUserRepoForQuota) ExistsByEmail(context.Context, string) (bool, err
func
(
r
*
stubUserRepoForQuota
)
RemoveGroupFromAllowedGroups
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
func
(
r
*
stubUserRepoForQuota
)
RemoveGroupFromAllowedGroups
(
context
.
Context
,
int64
)
(
int64
,
error
)
{
return
0
,
nil
return
0
,
nil
}
}
func
(
r
*
stubUserRepoForQuota
)
RemoveGroupFromUserAllowedGroups
(
context
.
Context
,
int64
,
int64
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForQuota
)
UpdateTotpSecret
(
context
.
Context
,
int64
,
*
string
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForQuota
)
UpdateTotpSecret
(
context
.
Context
,
int64
,
*
string
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForQuota
)
EnableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForQuota
)
EnableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForQuota
)
DisableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
r
*
stubUserRepoForQuota
)
DisableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
...
...
backend/internal/service/user_service.go
View file @
35291484
...
@@ -21,6 +21,7 @@ type UserListFilters struct {
...
@@ -21,6 +21,7 @@ type UserListFilters struct {
Status
string
// User status filter
Status
string
// User status filter
Role
string
// User role filter
Role
string
// User role filter
Search
string
// Search in email, username
Search
string
// Search in email, username
GroupName
string
// Filter by allowed group name (fuzzy match)
Attributes
map
[
int64
]
string
// Custom attribute filters: attributeID -> value
Attributes
map
[
int64
]
string
// Custom attribute filters: attributeID -> value
// IncludeSubscriptions controls whether ListWithFilters should load active subscriptions.
// IncludeSubscriptions controls whether ListWithFilters should load active subscriptions.
// For large datasets this can be expensive; admin list pages should enable it on demand.
// For large datasets this can be expensive; admin list pages should enable it on demand.
...
@@ -46,6 +47,8 @@ type UserRepository interface {
...
@@ -46,6 +47,8 @@ type UserRepository interface {
RemoveGroupFromAllowedGroups
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
RemoveGroupFromAllowedGroups
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
// AddGroupToAllowedGroups 将指定分组增量添加到用户的 allowed_groups(幂等,冲突忽略)
// AddGroupToAllowedGroups 将指定分组增量添加到用户的 allowed_groups(幂等,冲突忽略)
AddGroupToAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
AddGroupToAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
// RemoveGroupFromUserAllowedGroups 移除单个用户的指定分组权限
RemoveGroupFromUserAllowedGroups
(
ctx
context
.
Context
,
userID
int64
,
groupID
int64
)
error
// TOTP 双因素认证
// TOTP 双因素认证
UpdateTotpSecret
(
ctx
context
.
Context
,
userID
int64
,
encryptedSecret
*
string
)
error
UpdateTotpSecret
(
ctx
context
.
Context
,
userID
int64
,
encryptedSecret
*
string
)
error
...
...
backend/internal/service/user_service_test.go
View file @
35291484
...
@@ -46,7 +46,10 @@ func (m *mockUserRepo) RemoveGroupFromAllowedGroups(context.Context, int64) (int
...
@@ -46,7 +46,10 @@ func (m *mockUserRepo) RemoveGroupFromAllowedGroups(context.Context, int64) (int
return
0
,
nil
return
0
,
nil
}
}
func
(
m
*
mockUserRepo
)
AddGroupToAllowedGroups
(
context
.
Context
,
int64
,
int64
)
error
{
return
nil
}
func
(
m
*
mockUserRepo
)
AddGroupToAllowedGroups
(
context
.
Context
,
int64
,
int64
)
error
{
return
nil
}
func
(
m
*
mockUserRepo
)
UpdateTotpSecret
(
context
.
Context
,
int64
,
*
string
)
error
{
return
nil
}
func
(
m
*
mockUserRepo
)
RemoveGroupFromUserAllowedGroups
(
context
.
Context
,
int64
,
int64
)
error
{
return
nil
}
func
(
m
*
mockUserRepo
)
UpdateTotpSecret
(
context
.
Context
,
int64
,
*
string
)
error
{
return
nil
}
func
(
m
*
mockUserRepo
)
EnableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
m
*
mockUserRepo
)
EnableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
m
*
mockUserRepo
)
DisableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
func
(
m
*
mockUserRepo
)
DisableTotp
(
context
.
Context
,
int64
)
error
{
return
nil
}
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment