Skip to content
Open
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
54 changes: 48 additions & 6 deletions backend/internal/handler/admin/account_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2148,18 +2148,22 @@ func (h *AccountHandler) SetPrivacy(c *gin.Context) {
response.BadRequest(c, "Only OAuth accounts support privacy setting")
return
}
var mode string
var privacyResult service.PrivacySetResult
switch account.Platform {
case service.PlatformOpenAI:
mode = h.adminService.ForceOpenAIPrivacy(c.Request.Context(), account)
privacyResult = h.adminService.ForceOpenAIPrivacyDetailed(c.Request.Context(), account)
case service.PlatformAntigravity:
mode = h.adminService.ForceAntigravityPrivacy(c.Request.Context(), account)
privacyResult = h.adminService.ForceAntigravityPrivacyDetailed(c.Request.Context(), account)
default:
response.BadRequest(c, "Only OpenAI and Antigravity OAuth accounts support privacy setting")
return
}
if mode == "" {
response.BadRequest(c, "Cannot set privacy: missing access_token")
if privacyResult.Mode == "" {
message := privacyResult.Message
if message == "" {
message = "Cannot set privacy"
}
response.ErrorWithDetails(c, http.StatusBadRequest, message, privacyResult.Reason, privacyResultMetadata(account, privacyResult))
return
}
Comment thread
PA733 marked this conversation as resolved.
// 从 DB 重新读取以确保返回最新状态
Expand All @@ -2169,13 +2173,51 @@ func (h *AccountHandler) SetPrivacy(c *gin.Context) {
if account.Extra == nil {
account.Extra = make(map[string]any)
}
account.Extra["privacy_mode"] = mode
account.Extra["privacy_mode"] = privacyResult.Mode
if !privacyResult.Success {
response.ErrorWithDetails(c, http.StatusBadGateway, privacyResult.Message, privacyFailureReason(privacyResult), privacyResultMetadata(account, privacyResult))
return
}
Comment thread
PA733 marked this conversation as resolved.
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
return
}
if !privacyResult.Success {
response.ErrorWithDetails(c, http.StatusBadGateway, privacyResult.Message, privacyFailureReason(privacyResult), privacyResultMetadata(updated, privacyResult))
return
}
Comment thread
PA733 marked this conversation as resolved.
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), updated))
}

func privacyFailureReason(result service.PrivacySetResult) string {
if strings.TrimSpace(result.Reason) != "" {
return result.Reason
}
return "PRIVACY_SET_FAILED"
}

func privacyResultMetadata(account *service.Account, result service.PrivacySetResult) map[string]string {
metadata := map[string]string{
"privacy_mode": result.Mode,
"stage": result.Stage,
}
if account != nil {
metadata["account_id"] = strconv.FormatInt(account.ID, 10)
metadata["platform"] = account.Platform
}
if result.StatusCode != 0 {
metadata["status_code"] = strconv.Itoa(result.StatusCode)
}
if strings.TrimSpace(result.Detail) != "" {
metadata["detail"] = result.Detail
}
for key, value := range metadata {
if strings.TrimSpace(value) == "" {
delete(metadata, key)
}
}
return metadata
}

// RefreshTier handles refreshing Google One tier for a single account
// POST /api/v1/admin/accounts/:id/refresh-tier
func (h *AccountHandler) RefreshTier(c *gin.Context) {
Expand Down
126 changes: 126 additions & 0 deletions backend/internal/handler/admin/account_handler_privacy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package admin

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func setupAccountPrivacyRouter(adminSvc *stubAdminService) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
handler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
router.POST("/api/v1/admin/accounts/:id/set-privacy", handler.SetPrivacy)
return router
}

func TestAccountHandlerSetPrivacyNotExecutableReturnsBadRequest(t *testing.T) {
adminSvc := newStubAdminService()
adminSvc.getAccountValue = &service.Account{
ID: 11,
Name: "openai-oauth",
Platform: service.PlatformOpenAI,
Type: service.AccountTypeOAuth,
Status: service.StatusActive,
}
adminSvc.forceOpenAIPrivacy = service.PrivacySetResult{
Reason: "PRIVACY_SET_NOT_EXECUTABLE",
Message: "Cannot set privacy: missing access_token",
Stage: "precheck",
}

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/11/set-privacy", nil)
setupAccountPrivacyRouter(adminSvc).ServeHTTP(rec, req)

require.Equal(t, http.StatusBadRequest, rec.Code)
var body response.Response
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
require.Equal(t, http.StatusBadRequest, body.Code)
require.Equal(t, "Cannot set privacy: missing access_token", body.Message)
require.Equal(t, "PRIVACY_SET_NOT_EXECUTABLE", body.Reason)
require.Equal(t, map[string]string{
"account_id": "11",
"platform": service.PlatformOpenAI,
"stage": "precheck",
}, body.Metadata)
}

func TestAccountHandlerSetPrivacyFailureReturnsBadGatewayWithMetadata(t *testing.T) {
adminSvc := newStubAdminService()
adminSvc.getAccountValue = &service.Account{
ID: 12,
Name: "openai-oauth",
Platform: service.PlatformOpenAI,
Type: service.AccountTypeOAuth,
Status: service.StatusActive,
Extra: map[string]any{
"privacy_mode": service.PrivacyModeFailed,
},
}
adminSvc.forceOpenAIPrivacy = service.PrivacySetResult{
Mode: service.PrivacyModeFailed,
Reason: "PRIVACY_UPSTREAM_FAILED",
Message: "Privacy API returned a non-success response",
StatusCode: http.StatusUnauthorized,
Stage: "upstream_response",
Detail: "upstream said no",
}

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/12/set-privacy", nil)
setupAccountPrivacyRouter(adminSvc).ServeHTTP(rec, req)

require.Equal(t, http.StatusBadGateway, rec.Code)
var body response.Response
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
require.Equal(t, http.StatusBadGateway, body.Code)
require.Equal(t, "Privacy API returned a non-success response", body.Message)
require.Equal(t, "PRIVACY_UPSTREAM_FAILED", body.Reason)
require.Equal(t, map[string]string{
"account_id": "12",
"detail": "upstream said no",
"platform": service.PlatformOpenAI,
"privacy_mode": service.PrivacyModeFailed,
"stage": "upstream_response",
"status_code": "401",
}, body.Metadata)
}

func TestAccountHandlerSetPrivacySuccessReturnsAccount(t *testing.T) {
adminSvc := newStubAdminService()
adminSvc.getAccountValue = &service.Account{
ID: 13,
Name: "openai-oauth",
Platform: service.PlatformOpenAI,
Type: service.AccountTypeOAuth,
Status: service.StatusActive,
Extra: map[string]any{
"privacy_mode": service.PrivacyModeTrainingOff,
},
}
adminSvc.forceOpenAIPrivacy = service.PrivacySetResult{
Mode: service.PrivacyModeTrainingOff,
Success: true,
Message: "Training data sharing disabled",
Stage: "upstream_response",
}

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/13/set-privacy", nil)
setupAccountPrivacyRouter(adminSvc).ServeHTTP(rec, req)

require.Equal(t, http.StatusOK, rec.Code)
var body response.Response
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
require.Equal(t, 0, body.Code)
require.Equal(t, "success", body.Message)
require.Empty(t, body.Reason)
require.Nil(t, body.Metadata)
}
15 changes: 15 additions & 0 deletions backend/internal/handler/admin/admin_service_stub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ type stubAdminService struct {
updateAccountErr error
bulkUpdateAccountErr error
checkMixedErr error
getAccountValue *service.Account
forceOpenAIPrivacy service.PrivacySetResult
forceAGPrivacy service.PrivacySetResult
lastMixedCheck struct {
accountID int64
platform string
Expand Down Expand Up @@ -324,6 +327,10 @@ func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int,
}

func (s *stubAdminService) GetAccount(ctx context.Context, id int64) (*service.Account, error) {
if s.getAccountValue != nil {
account := *s.getAccountValue
return &account, nil
}
account := service.Account{ID: id, Name: "account", Status: service.StatusActive}
return &account, nil
}
Expand Down Expand Up @@ -616,10 +623,18 @@ func (s *stubAdminService) ForceOpenAIPrivacy(ctx context.Context, account *serv
return ""
}

func (s *stubAdminService) ForceOpenAIPrivacyDetailed(ctx context.Context, account *service.Account) service.PrivacySetResult {
return s.forceOpenAIPrivacy
}

func (s *stubAdminService) ForceAntigravityPrivacy(ctx context.Context, account *service.Account) string {
return ""
}

func (s *stubAdminService) ForceAntigravityPrivacyDetailed(ctx context.Context, account *service.Account) service.PrivacySetResult {
return s.forceAGPrivacy
}

func (s *stubAdminService) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*service.ReplaceUserGroupResult, error) {
return &service.ReplaceUserGroupResult{MigratedKeys: 0}, nil
}
Expand Down
83 changes: 64 additions & 19 deletions backend/internal/service/admin_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ type AdminService interface {
EnsureAntigravityPrivacy(ctx context.Context, account *Account) string
// ForceOpenAIPrivacy 强制重新设置 OpenAI OAuth 账号隐私,无论当前状态。
ForceOpenAIPrivacy(ctx context.Context, account *Account) string
ForceOpenAIPrivacyDetailed(ctx context.Context, account *Account) PrivacySetResult
// ForceAntigravityPrivacy 强制重新设置 Antigravity OAuth 账号隐私,无论当前状态。
ForceAntigravityPrivacy(ctx context.Context, account *Account) string
ForceAntigravityPrivacyDetailed(ctx context.Context, account *Account) PrivacySetResult
SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*Account, error)
BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error)
CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error
Expand Down Expand Up @@ -3702,16 +3704,32 @@ func (s *adminServiceImpl) EnsureOpenAIPrivacy(ctx context.Context, account *Acc

// ForceOpenAIPrivacy 强制重新设置 OpenAI OAuth 账号隐私,无论当前状态。
func (s *adminServiceImpl) ForceOpenAIPrivacy(ctx context.Context, account *Account) string {
return s.ForceOpenAIPrivacyDetailed(ctx, account).Mode
}

func (s *adminServiceImpl) ForceOpenAIPrivacyDetailed(ctx context.Context, account *Account) PrivacySetResult {
if account.Platform != PlatformOpenAI || account.Type != AccountTypeOAuth {
return ""
return PrivacySetResult{
Reason: "PRIVACY_UNSUPPORTED_ACCOUNT",
Message: "Only OpenAI OAuth accounts support OpenAI privacy setting",
Stage: "precheck",
}
}
if s.privacyClientFactory == nil {
return ""
return PrivacySetResult{
Reason: "PRIVACY_CLIENT_UNAVAILABLE",
Message: "Privacy client is not configured",
Stage: "precheck",
}
}

token, _ := account.Credentials["access_token"].(string)
if token == "" {
return ""
return PrivacySetResult{
Reason: "PRIVACY_SET_NOT_EXECUTABLE",
Message: "Cannot set privacy: missing access_token",
Stage: "precheck",
}
}

var proxyURL string
Expand All @@ -3721,20 +3739,24 @@ func (s *adminServiceImpl) ForceOpenAIPrivacy(ctx context.Context, account *Acco
}
}

mode := disableOpenAITraining(ctx, s.privacyClientFactory, token, proxyURL)
if mode == "" {
return ""
result := disableOpenAITrainingDetailed(ctx, s.privacyClientFactory, token, proxyURL)
if result.Mode == "" {
return result
}

if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil {
if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": result.Mode}); err != nil {
logger.LegacyPrintf("service.admin", "force_update_openai_privacy_mode_failed: account_id=%d err=%v", account.ID, err)
return mode
result.Success = false
result.Reason = "PRIVACY_PERSIST_FAILED"
result.Message = "Privacy setting completed upstream but failed to persist privacy_mode"
result.Detail = strings.TrimSpace(result.Detail + " persist privacy_mode failed: " + err.Error())
return result
}
if account.Extra == nil {
account.Extra = make(map[string]any)
}
account.Extra["privacy_mode"] = mode
return mode
account.Extra["privacy_mode"] = result.Mode
return result
}

// EnsureAntigravityPrivacy 检查 Antigravity OAuth 账号隐私状态。
Expand Down Expand Up @@ -3779,16 +3801,35 @@ func (s *adminServiceImpl) EnsureAntigravityPrivacy(ctx context.Context, account

// ForceAntigravityPrivacy 强制重新设置 Antigravity OAuth 账号隐私,无论当前状态。
func (s *adminServiceImpl) ForceAntigravityPrivacy(ctx context.Context, account *Account) string {
return s.ForceAntigravityPrivacyDetailed(ctx, account).Mode
}

func (s *adminServiceImpl) ForceAntigravityPrivacyDetailed(ctx context.Context, account *Account) PrivacySetResult {
if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth {
return ""
return PrivacySetResult{
Reason: "PRIVACY_UNSUPPORTED_ACCOUNT",
Message: "Only Antigravity OAuth accounts support Antigravity privacy setting",
Stage: "precheck",
}
}

token, _ := account.Credentials["access_token"].(string)
if token == "" {
return ""
return PrivacySetResult{
Reason: "PRIVACY_SET_NOT_EXECUTABLE",
Message: "Cannot set privacy: missing access_token",
Stage: "precheck",
}
}

projectID, _ := account.Credentials["project_id"].(string)
if strings.TrimSpace(projectID) == "" {
return PrivacySetResult{
Reason: "PRIVACY_MISSING_PROJECT_ID",
Message: "Cannot verify privacy setting: missing project_id",
Stage: "precheck",
}
}

var proxyURL string
if account.ProxyID != nil {
Expand All @@ -3797,15 +3838,19 @@ func (s *adminServiceImpl) ForceAntigravityPrivacy(ctx context.Context, account
}
}

mode := setAntigravityPrivacy(ctx, token, projectID, proxyURL)
if mode == "" {
return ""
result := setAntigravityPrivacyDetailed(ctx, token, projectID, proxyURL)
if result.Mode == "" {
return result
}

if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil {
if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": result.Mode}); err != nil {
logger.LegacyPrintf("service.admin", "force_update_antigravity_privacy_mode_failed: account_id=%d err=%v", account.ID, err)
return mode
result.Success = false
result.Reason = "PRIVACY_PERSIST_FAILED"
result.Message = "Privacy setting completed upstream but failed to persist privacy_mode"
result.Detail = strings.TrimSpace(result.Detail + " persist privacy_mode failed: " + err.Error())
return result
}
applyAntigravityPrivacyMode(account, mode)
return mode
applyAntigravityPrivacyMode(account, result.Mode)
return result
}
Loading
Loading