mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
Merge pull request #3047 from fchange/codex/delete-user-api-keys
fix(admin): delete API keys when deleting a user
This commit is contained in:
@@ -313,6 +313,10 @@ func (r *apiKeyRepository) Delete(ctx context.Context, id int64) error {
|
||||
func (r *apiKeyRepository) DeleteWithAudit(ctx context.Context, id int64) error {
|
||||
tombstoneKey := fmt.Sprintf("__deleted__%d__%d", id, time.Now().UnixNano())
|
||||
|
||||
if existingTx := dbent.TxFromContext(ctx); existingTx != nil {
|
||||
return r.deleteWithAudit(ctx, existingTx.Client(), id, tombstoneKey)
|
||||
}
|
||||
|
||||
tx, err := r.client.Tx(ctx)
|
||||
if err != nil && !errors.Is(err, dbent.ErrTxStarted) {
|
||||
return err
|
||||
@@ -322,8 +326,18 @@ func (r *apiKeyRepository) DeleteWithAudit(ctx context.Context, id int64) error
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
exec = tx.Client()
|
||||
}
|
||||
// err == dbent.ErrTxStarted 时复用当前事务(exec = r.client)。
|
||||
|
||||
if err := r.deleteWithAudit(ctx, exec, id, tombstoneKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tx != nil {
|
||||
return tx.Commit()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *apiKeyRepository) deleteWithAudit(ctx context.Context, exec *dbent.Client, id int64, tombstoneKey string) error {
|
||||
// 1. 审计:数据源即 api_keys 当前行;WHERE deleted_at IS NULL 保证只对未删除行写一次。
|
||||
if _, err := exec.ExecContext(ctx, `
|
||||
INSERT INTO deleted_api_key_audits (key, api_key_id, user_id, key_name, deleted_at)
|
||||
@@ -358,10 +372,6 @@ func (r *apiKeyRepository) DeleteWithAudit(ctx context.Context, id int64) error
|
||||
}
|
||||
return service.ErrAPIKeyNotFound
|
||||
}
|
||||
|
||||
if tx != nil {
|
||||
return tx.Commit()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -834,16 +834,88 @@ func (s *adminServiceImpl) DeleteUser(ctx context.Context, id int64) error {
|
||||
if user.Role == "admin" {
|
||||
return errors.New("cannot delete admin user")
|
||||
}
|
||||
if err := s.userRepo.Delete(ctx, id); err != nil {
|
||||
logger.LegacyPrintf("service.admin", "delete user failed: user_id=%d err=%v", id, err)
|
||||
|
||||
apiKeys, err := s.listUserAPIKeysForDeletion(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.entClient != nil {
|
||||
tx, err := s.entClient.Tx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
opCtx := dbent.NewTxContext(ctx, tx)
|
||||
if err := s.deleteUserWithAPIKeys(opCtx, id, apiKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := s.deleteUserWithAPIKeys(ctx, id, apiKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.authCacheInvalidator != nil {
|
||||
for _, key := range apiKeys {
|
||||
if keyValue := strings.TrimSpace(key.Key); keyValue != "" {
|
||||
s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, keyValue)
|
||||
}
|
||||
}
|
||||
s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) listUserAPIKeysForDeletion(ctx context.Context, userID int64) ([]APIKey, error) {
|
||||
if s.apiKeyRepo == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const pageSize = 1000
|
||||
keys := make([]APIKey, 0)
|
||||
for page := 1; ; page++ {
|
||||
batch, result, err := s.apiKeyRepo.ListByUserID(ctx, userID, pagination.PaginationParams{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
SortBy: "id",
|
||||
SortOrder: pagination.SortOrderAsc,
|
||||
}, APIKeyListFilters{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list user api keys: %w", err)
|
||||
}
|
||||
keys = append(keys, batch...)
|
||||
if len(batch) == 0 || len(batch) < pageSize || result == nil || int64(len(keys)) >= result.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) deleteUserWithAPIKeys(ctx context.Context, userID int64, apiKeys []APIKey) error {
|
||||
if s.apiKeyRepo != nil {
|
||||
for _, key := range apiKeys {
|
||||
if key.ID <= 0 {
|
||||
continue
|
||||
}
|
||||
if err := s.apiKeyRepo.DeleteWithAudit(ctx, key.ID); err != nil {
|
||||
logger.LegacyPrintf("service.admin", "delete user api key failed: user_id=%d api_key_id=%d err=%v", userID, key.ID, err)
|
||||
return fmt.Errorf("delete user api key %d: %w", key.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.userRepo.Delete(ctx, userID); err != nil {
|
||||
logger.LegacyPrintf("service.admin", "delete user failed: user_id=%d err=%v", userID, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) BatchUpdateConcurrency(ctx context.Context, userIDs []int64, value int, mode string) (int, error) {
|
||||
cleaned := make([]int64, 0, len(userIDs))
|
||||
for _, uid := range userIDs {
|
||||
|
||||
@@ -515,6 +515,31 @@ func TestAdminService_DeleteUser_Success(t *testing.T) {
|
||||
require.Equal(t, []int64{7}, repo.deletedIDs)
|
||||
}
|
||||
|
||||
func TestAdminService_DeleteUser_DeletesOwnedAPIKeys(t *testing.T) {
|
||||
repo := &userRepoStub{user: &User{ID: 7, Role: RoleUser}}
|
||||
apiKeyRepo := &apiKeyRepoStub{
|
||||
allowListByUserID: true,
|
||||
listByUserIDKeys: []APIKey{
|
||||
{ID: 11, UserID: 7, Key: "sk-user-1"},
|
||||
{ID: 12, UserID: 7, Key: "sk-user-2"},
|
||||
},
|
||||
}
|
||||
invalidator := &authCacheInvalidatorStub{}
|
||||
svc := &adminServiceImpl{
|
||||
userRepo: repo,
|
||||
apiKeyRepo: apiKeyRepo,
|
||||
authCacheInvalidator: invalidator,
|
||||
}
|
||||
|
||||
err := svc.DeleteUser(context.Background(), 7)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []int64{7}, repo.deletedIDs)
|
||||
require.Equal(t, []int64{7}, apiKeyRepo.listByUserIDCalls)
|
||||
require.Equal(t, []int64{11, 12}, apiKeyRepo.deletedIDs)
|
||||
require.ElementsMatch(t, []string{"sk-user-1", "sk-user-2"}, invalidator.keys)
|
||||
require.Equal(t, []int64{7}, invalidator.userIDs)
|
||||
}
|
||||
|
||||
func TestAdminService_DeleteUser_NotFound(t *testing.T) {
|
||||
repo := &userRepoStub{getErr: ErrUserNotFound}
|
||||
svc := &adminServiceImpl{userRepo: repo}
|
||||
|
||||
@@ -24,13 +24,18 @@ import (
|
||||
// - deleteErr: 模拟 Delete 返回的错误
|
||||
// - deletedIDs: 记录被调用删除的 API Key ID,用于断言验证
|
||||
type apiKeyRepoStub struct {
|
||||
apiKey *APIKey // GetKeyAndOwnerID 的返回值
|
||||
getByIDErr error // GetKeyAndOwnerID 的错误返回值
|
||||
deleteErr error // Delete 的错误返回值
|
||||
deletedIDs []int64 // 记录已删除的 API Key ID 列表
|
||||
updateLastUsed func(ctx context.Context, id int64, usedAt time.Time) error
|
||||
touchedIDs []int64
|
||||
touchedUsedAts []time.Time
|
||||
apiKey *APIKey // GetKeyAndOwnerID 的返回值
|
||||
getByIDErr error // GetKeyAndOwnerID 的错误返回值
|
||||
deleteErr error // Delete 的错误返回值
|
||||
deletedIDs []int64 // 记录已删除的 API Key ID 列表
|
||||
allowListByUserID bool
|
||||
listByUserIDKeys []APIKey
|
||||
listByUserIDErr error
|
||||
listByUserIDCalls []int64
|
||||
listByUserIDParams []pagination.PaginationParams
|
||||
updateLastUsed func(ctx context.Context, id int64, usedAt time.Time) error
|
||||
touchedIDs []int64
|
||||
touchedUsedAts []time.Time
|
||||
}
|
||||
|
||||
// 以下方法在本测试中不应被调用,使用 panic 确保测试失败时能快速定位问题
|
||||
@@ -88,7 +93,21 @@ func (s *apiKeyRepoStub) DeleteWithAudit(ctx context.Context, id int64) error {
|
||||
// 以下是接口要求实现但本测试不关心的方法
|
||||
|
||||
func (s *apiKeyRepoStub) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, filters APIKeyListFilters) ([]APIKey, *pagination.PaginationResult, error) {
|
||||
panic("unexpected ListByUserID call")
|
||||
if !s.allowListByUserID {
|
||||
panic("unexpected ListByUserID call")
|
||||
}
|
||||
s.listByUserIDCalls = append(s.listByUserIDCalls, userID)
|
||||
s.listByUserIDParams = append(s.listByUserIDParams, params)
|
||||
if s.listByUserIDErr != nil {
|
||||
return nil, nil, s.listByUserIDErr
|
||||
}
|
||||
keys := append([]APIKey(nil), s.listByUserIDKeys...)
|
||||
return keys, &pagination.PaginationResult{
|
||||
Total: int64(len(keys)),
|
||||
Page: params.Page,
|
||||
PageSize: params.PageSize,
|
||||
Pages: 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *apiKeyRepoStub) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
|
||||
|
||||
Reference in New Issue
Block a user