diff --git a/internal/account/store.go b/internal/account/store.go index e9215ec..947864a 100644 --- a/internal/account/store.go +++ b/internal/account/store.go @@ -45,7 +45,6 @@ type Account struct { Cookies string `json:"-"` CredentialReference CredentialReference `json:"-"` CredentialKey string `json:"-"` - AuthorizationStatus string `json:"authorization_status"` RuntimeStatus string `json:"runtime_status"` Version int64 `json:"version"` } @@ -154,8 +153,8 @@ func (s *Store) CreateAccount(ctx context.Context, account Account, credentials defer tx.Rollback() if _, err := tx.ExecContext(ctx, ` INSERT INTO social_account - (account_id, credential_provider, credential_key, name, platform, platform_account_key, tags, authorization_kind, authorization_status, status) - VALUES ($1, $2, $3, $4, $5, $6, $7, 'owned', 'authorized', 'paused')`, account.ID, + (account_id, credential_provider, credential_key, name, platform, platform_account_key, tags, status) + VALUES ($1, $2, $3, $4, $5, $6, $7, 'paused')`, account.ID, account.CredentialReference.Provider, account.CredentialKey, account.Name, account.Platform, account.PlatformAccountKey, account.Tags); err != nil { return publicDatabaseError(err) @@ -189,7 +188,7 @@ func commitKnownRolledBack(err error) bool { func (s *Store) ListAccounts(ctx context.Context) ([]Account, error) { rows, err := s.db.QueryContext(ctx, ` SELECT account.account_id, account.name, account.platform, account.platform_account_key, account.tags, - account.authorization_status, account.status, account.version + account.status, account.version FROM social_account account ORDER BY account.created_at, account.account_id`) if err != nil { @@ -213,7 +212,7 @@ func (s *Store) GetAccount(ctx context.Context, id string) (Account, error) { } return scanAccount(s.db.QueryRowContext(ctx, ` SELECT account.account_id, account.name, account.platform, account.platform_account_key, account.tags, - account.authorization_status, account.status, account.version + account.status, account.version FROM social_account account WHERE account.account_id = $1`, id)) } @@ -246,7 +245,7 @@ func scanAccount(row accountScanner) (Account, error) { var account Account var tags pgtype.FlatArray[string] if err := row.Scan(&account.ID, &account.Name, &account.Platform, &account.PlatformAccountKey, pgtype.NewMap().SQLScanner(&tags), - &account.AuthorizationStatus, &account.RuntimeStatus, &account.Version); err != nil { + &account.RuntimeStatus, &account.Version); err != nil { return Account{}, rowError(err) } account.Tags = []string(tags) @@ -282,14 +281,6 @@ func validAccount(account Account) bool { } func (s *Store) PauseAccount(ctx context.Context, accountID string) error { - return s.disableAccount(ctx, accountID, false) -} - -func (s *Store) RevokeAccount(ctx context.Context, accountID string) error { - return s.disableAccount(ctx, accountID, true) -} - -func (s *Store) disableAccount(ctx context.Context, accountID string, revoke bool) error { if !idPattern.MatchString(accountID) { return ErrInvalid } @@ -299,32 +290,21 @@ func (s *Store) disableAccount(ctx context.Context, accountID string, revoke boo } defer tx.Rollback() var version int64 - var authorizationStatus, runtimeStatus string - if err := tx.QueryRowContext(ctx, `SELECT version, authorization_status, status FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID). - Scan(&version, &authorizationStatus, &runtimeStatus); err != nil { + var runtimeStatus string + if err := tx.QueryRowContext(ctx, `SELECT version, status FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID). + Scan(&version, &runtimeStatus); err != nil { return rowError(err) } - unchanged := (revoke && authorizationStatus == "revoked") || (!revoke && runtimeStatus == "paused") + unchanged := runtimeStatus == "paused" if !unchanged { if err := tx.QueryRowContext(ctx, ` UPDATE social_account - SET authorization_status = CASE WHEN $2 THEN 'revoked' ELSE authorization_status END, - status = 'paused', paused_at = now(), revoked_at = CASE WHEN $2 THEN now() ELSE revoked_at END, + SET status = 'paused', paused_at = now(), version = version + 1, updated_at = now() - WHERE account_id = $1 RETURNING version`, accountID, revoke).Scan(&version); err != nil { + WHERE account_id = $1 RETURNING version`, accountID).Scan(&version); err != nil { return errors.New("change account state") } - } - reason := "account_paused" - if revoke { - reason = "account_revoked" - } - if !unchanged { - eventType := "account_paused" - if revoke { - eventType = "account_revoked" - } - if err := appendAudit(ctx, tx, eventType, reason, accountID, map[string]any{ + if err := appendAudit(ctx, tx, "account_paused", "account_paused", accountID, map[string]any{ "account_version": version, }); err != nil { return err @@ -342,14 +322,11 @@ func (s *Store) ResumeAccount(ctx context.Context, accountID string) error { return errors.New("begin resume transaction") } defer tx.Rollback() - var authorizationStatus, runtimeStatus string - if err := tx.QueryRowContext(ctx, `SELECT authorization_status, status FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID). - Scan(&authorizationStatus, &runtimeStatus); err != nil { + var runtimeStatus string + if err := tx.QueryRowContext(ctx, `SELECT status FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID). + Scan(&runtimeStatus); err != nil { return rowError(err) } - if authorizationStatus == "revoked" { - return ErrConflict - } var ready bool if err := tx.QueryRowContext(ctx, ` SELECT EXISTS ( diff --git a/internal/account/store_test.go b/internal/account/store_test.go index aa9a8fe..a3ca02c 100644 --- a/internal/account/store_test.go +++ b/internal/account/store_test.go @@ -263,7 +263,7 @@ func TestAccountStoreLifecycleAgainstPostgres(t *testing.T) { t.Fatalf("list accounts: %d err=%v", len(accounts), err) } account, err := store.GetAccount(ctx, "account-lifecycle-a") - if err != nil || account.RuntimeStatus != "paused" || account.AuthorizationStatus != "authorized" || len(account.Tags) != 1 { + if err != nil || account.RuntimeStatus != "paused" || len(account.Tags) != 1 { t.Fatalf("get account: %#v err=%v", account, err) } if _, err := store.GetAccount(ctx, "account-invalid"); !errors.Is(err, ErrInvalid) && !errors.Is(err, ErrNotFound) { @@ -312,15 +312,6 @@ func TestAccountStoreLifecycleAgainstPostgres(t *testing.T) { if account, err := store.GetAccount(ctx, "account-lifecycle-a"); err != nil || account.RuntimeStatus != "paused" { t.Fatalf("paused account: %#v err=%v", account, err) } - if err := store.RevokeAccount(ctx, "account-lifecycle-b"); err != nil { - t.Fatalf("revoke: %v", err) - } - if err := store.ResumeAccount(ctx, "account-lifecycle-b"); !errors.Is(err, ErrConflict) { - t.Fatalf("resume revoked account must conflict: %v", err) - } - if err := store.RevokeAccount(ctx, "account-lifecycle-b"); err != nil { - t.Fatalf("revoke must stay idempotent: %v", err) - } // 审计:分页 + 过滤 + 非法过滤参数 page, err := store.ListAudit(ctx, AuditFilter{AccountID: "account-lifecycle-a", Page: 1, PageSize: 10}) diff --git a/internal/controlplane/api/account_environment_unit_test.go b/internal/controlplane/api/account_environment_unit_test.go index c7b5483..5a88021 100644 --- a/internal/controlplane/api/account_environment_unit_test.go +++ b/internal/controlplane/api/account_environment_unit_test.go @@ -110,18 +110,6 @@ func TestAccountEnvironmentAutoBindingAndStart(t *testing.T) { t.Fatalf("start audit pair missing: rows=%d err=%v", startAudits, err) } - // start 冲突分支:吊销账号后 start → 409 readiness blocked - if response := do(app, http.MethodPost, "/api/phase-a/accounts/"+accountID+"/revoke", ""); response.Code != http.StatusNoContent { - t.Fatalf("revoke failed: %d: %s", response.Code, response.Body.String()) - } - response = do(app, http.MethodPost, "/api/phase-a/accounts/"+accountID+"/start", "") - if response.Code != http.StatusConflict { - t.Fatalf("expected 409 start on revoked account, got %d: %s", response.Code, response.Body.String()) - } - var conflict map[string]string - if err := json.Unmarshal(response.Body.Bytes(), &conflict); err != nil || conflict["reason_code"] != "account_revoked" || conflict["readiness"] != "blocked" { - t.Fatalf("start conflict payload: %s err=%v", response.Body.String(), err) - } } func TestAccountEnvironmentDeletionLifecycle(t *testing.T) { @@ -234,8 +222,8 @@ func TestAccountAuditEndpointFilters(t *testing.T) { t.Cleanup(func() { _ = auditDB.Close() }) if _, err := auditDB.ExecContext(ctx, ` INSERT INTO gateway (name, endpoint, token) VALUES ('gw-1', 'http://gw-1:8081', 'unit-test-gateway-token'); - INSERT INTO social_account (account_id, credential_provider, credential_key, name, platform, platform_account_key, authorization_kind, authorization_status, status) - VALUES ('account-audit-0000000000000000', 'os_keyring', 'creatorhub/account-audit-0000000000000000', '审计账号', 'douyin', 'key-audit', 'owned', 'authorized', 'paused'); + INSERT INTO social_account (account_id, credential_provider, credential_key, name, platform, platform_account_key, status) + VALUES ('account-audit-0000000000000000', 'os_keyring', 'creatorhub/account-audit-0000000000000000', '审计账号', 'douyin', 'key-audit', 'paused'); INSERT INTO audit_event (event_type, account_id, actor, reason_code, details) SELECT 'account_created', account.id, 'local-user', 'account_created', '{"platform":"douyin"}' FROM social_account account WHERE account.account_id = 'account-audit-0000000000000000'`); err != nil { diff --git a/internal/controlplane/api/accounts_operations.go b/internal/controlplane/api/accounts_operations.go index 1775b2b..28c3129 100644 --- a/internal/controlplane/api/accounts_operations.go +++ b/internal/controlplane/api/accounts_operations.go @@ -49,7 +49,7 @@ func RegisterAccountRoutes(app *fiber.App, store *accountdomain.Store, runtimeSt PlatformAccountKey: strings.TrimSpace(input.PlatformAccountKey), Tags: tags, Cookies: strings.TrimSpace(input.Cookies), CredentialReference: accountdomain.CredentialReference{ID: accountID + "-cookies", Provider: "os_keyring"}, CredentialKey: "creatorhub/" + accountID + "/cookies", - AuthorizationStatus: "authorized", RuntimeStatus: "paused", Version: 1, + RuntimeStatus: "paused", Version: 1, } if err := store.CreateAccount(c.Context(), account, credentials); err != nil { if errors.Is(err, accountdomain.ErrAccountCreationUnknown) { @@ -165,23 +165,6 @@ func RegisterAccountRoutes(app *fiber.App, store *accountdomain.Store, runtimeSt return c.SendStatus(fiber.StatusNoContent) }) - app.Post("/api/phase-a/accounts/:id/revoke", func(c fiber.Ctx) error { - unlock, err := lockAccountResources(c.Context(), runtimeStore, c.Params("id")) - if err != nil { - return hubError(c, err) - } - defer unlock() - if err := store.RevokeAccount(c.Context(), c.Params("id")); err != nil { - return phaseAError(c, err) - } - if runtimeStore != nil { - if err := stopAccountRuntime(c.Context(), runtimeStore, c.Params("id")); err != nil { - return hubError(c, err) - } - } - return c.SendStatus(fiber.StatusNoContent) - }) - app.Get("/api/phase-a/audit", func(c fiber.Ctx) error { filter, err := auditFilter(c) if err != nil { @@ -234,8 +217,6 @@ func auditFilter(c fiber.Ctx) (accountdomain.AuditFilter, error) { func resumeBlockReason(account accountdomain.Account, environment hub.EnvironmentContext, bindingFound bool) string { switch { - case account.AuthorizationStatus != "authorized": - return "account_revoked" case !bindingFound: return "binding_missing" case environment.Exit.ID != "" && environment.Exit.HealthStatus != "healthy": @@ -328,8 +309,8 @@ func accountDetailPayload(store *accountdomain.Store, runtimeStore HubStore) fib payload := map[string]any{ "id": account.ID, "name": account.Name, "platform": account.Platform, "platform_account_key": account.PlatformAccountKey, "tags": account.Tags, - "authorization_status": account.AuthorizationStatus, "runtime_status": account.RuntimeStatus, - "version": account.Version, + "runtime_status": account.RuntimeStatus, + "version": account.Version, } if runtimeStore != nil { if environment, envErr := runtimeStore.GetEnvironmentContextForAccount(c.Context(), account.ID); envErr == nil { diff --git a/internal/controlplane/api/creator.go b/internal/controlplane/api/creator.go index 6a52203..0d74231 100644 --- a/internal/controlplane/api/creator.go +++ b/internal/controlplane/api/creator.go @@ -735,8 +735,7 @@ func creatorLoginQRCode(ctx context.Context, store *creator.Store, phaseAStore * return nil, err } if account.Platform != creator.PlatformDouyin || profile.Platform != creator.PlatformDouyin || - account.AuthorizationStatus != "authorized" || profile.PlatformAccountKey == "" || - account.PlatformAccountKey != profile.PlatformAccountKey { + profile.PlatformAccountKey == "" || account.PlatformAccountKey != profile.PlatformAccountKey { return nil, creator.ErrConflict } unlock, lockErr := lockAccountResources(ctx, hubStore, accountID) @@ -784,7 +783,7 @@ func verifyCreatorAccount(ctx context.Context, store *creator.Store, phaseAStore if err != nil { return creator.LoginResult{}, err } - if account.Platform != profile.Platform || account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" || profile.PlatformAccountKey == "" || account.PlatformAccountKey != profile.PlatformAccountKey { + if account.Platform != profile.Platform || account.Platform != creator.PlatformDouyin || profile.PlatformAccountKey == "" || account.PlatformAccountKey != profile.PlatformAccountKey { return creator.LoginResult{}, creator.ErrConflict } environment, err := hubStore.GetEnvironmentContextForAccount(ctx, accountID) @@ -1221,7 +1220,7 @@ func newDouyinAccountBrowser(ctx context.Context, store *creator.Store, phaseASt if err != nil { return creatorGatewayBrowser{}, err } - if account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" { + if account.Platform != creator.PlatformDouyin { return creatorGatewayBrowser{}, creator.ErrConflict } profile, err := store.GetAccountProfile(ctx, accountID) @@ -1536,7 +1535,7 @@ func refreshCreatorMetricWork(ctx context.Context, store *creator.Store, phaseAS if err != nil { return err } - if account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" || (profile.BusinessStatus != "normal" && profile.BusinessStatus != "muted") || profile.LoginStatus != "logged_in" { + if account.Platform != creator.PlatformDouyin || (profile.BusinessStatus != "normal" && profile.BusinessStatus != "muted") || profile.LoginStatus != "logged_in" { return creator.ErrConflict } environment, err := hubStore.GetEnvironmentContextForAccount(ctx, accountID) @@ -1600,7 +1599,7 @@ func syncCreatorOwned(ctx context.Context, store *creator.Store, phaseAStore *ac if err != nil { return err } - if account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" { + if account.Platform != creator.PlatformDouyin { return creator.ErrConflict } profile, err := store.GetAccountProfile(ctx, accountID) @@ -1676,7 +1675,7 @@ func creatorCollectionAccount(ctx context.Context, store *creator.Store, phaseAS return "", err } for _, account := range accounts { - if account.Platform != platform || account.AuthorizationStatus != "authorized" { + if account.Platform != platform { continue } profile, err := store.GetAccountProfile(ctx, account.ID) diff --git a/internal/controlplane/api/douyin_test.go b/internal/controlplane/api/douyin_test.go index 003d121..805aee3 100644 --- a/internal/controlplane/api/douyin_test.go +++ b/internal/controlplane/api/douyin_test.go @@ -108,7 +108,7 @@ func TestDouyinGatewayBrowserDoesNotEchoCredentialOnFailure(t *testing.T) { func readyDouyinEnvironment() hub.EnvironmentContext { return hub.EnvironmentContext{Env: hub.Env{Alias: "account-a", Gateway: "gateway-a"}, AccountID: "account-a", - AccountStatus: "active", AuthorizationStatus: "authorized", BindingVersion: 2, + AccountStatus: "active", BindingVersion: 2, Exit: hub.NetworkExit{ID: "exit-a", HealthStatus: "healthy"}, RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} } diff --git a/internal/controlplane/api/environments.go b/internal/controlplane/api/environments.go index 1ba5fe6..eb6b7cb 100644 --- a/internal/controlplane/api/environments.go +++ b/internal/controlplane/api/environments.go @@ -333,7 +333,6 @@ type envView struct { Endpoint string `json:"endpoint"` AccountID string `json:"account_id"` AccountStatus string `json:"account_status"` - AuthorizationStatus string `json:"authorization_status"` NetworkExitID string `json:"network_exit_id"` NetworkExitHealth string `json:"network_exit_health"` BindingVersion int64 `json:"binding_version"` @@ -350,8 +349,6 @@ type envView struct { func environmentScheduleReadiness(environment hub.EnvironmentContext) (string, string) { switch { - case environment.AuthorizationStatus != "authorized": - return "blocked", "account_revoked" case environment.AccountStatus != "active": return "blocked", "account_paused" case environment.Exit.ID != "" && environment.Exit.HealthStatus != "healthy": @@ -392,7 +389,7 @@ func validCreatedRuntime(created runtimeStatus, environment hub.EnvironmentConte } func accountRunnable(environment hub.EnvironmentContext) bool { - return environment.AccountStatus == "active" && environment.AuthorizationStatus == "authorized" + return environment.AccountStatus == "active" } func releaseRuntime(ctx context.Context, store RuntimeStopStore, environment hub.EnvironmentContext) error { @@ -525,7 +522,7 @@ func getBrowser(store HubStore) fiber.Handler { view := envView{ Env: environment.Env, State: "recorded", Status: "已记录运行实例", AccountID: environment.AccountID, AccountStatus: environment.AccountStatus, - AuthorizationStatus: environment.AuthorizationStatus, NetworkExitID: environment.Exit.ID, + NetworkExitID: environment.Exit.ID, NetworkExitHealth: environment.Exit.HealthStatus, BindingVersion: environment.BindingVersion, RuntimeID: environment.RuntimeID, RuntimeNodeID: environment.RuntimeNodeID, ScheduleStatus: "blocked", ScheduleBlockReason: "binding_missing", @@ -885,7 +882,7 @@ func listBrowsers(store HubStore) fiber.Handler { RecoveryRequired: true, GatewayReachable: false, } if environment, contextErr := store.GetEnvironmentContext(c.Context(), env.Alias); contextErr == nil { - view.AccountID, view.AccountStatus, view.AuthorizationStatus = environment.AccountID, environment.AccountStatus, environment.AuthorizationStatus + view.AccountID, view.AccountStatus = environment.AccountID, environment.AccountStatus view.NetworkExitID, view.NetworkExitHealth, view.BindingVersion = environment.Exit.ID, environment.Exit.HealthStatus, environment.BindingVersion view.RuntimeID = environment.RuntimeID view.RuntimeID = environment.RuntimeID diff --git a/internal/controlplane/api/gateway_browser_unit_test.go b/internal/controlplane/api/gateway_browser_unit_test.go index 204e850..1550cbf 100644 --- a/internal/controlplane/api/gateway_browser_unit_test.go +++ b/internal/controlplane/api/gateway_browser_unit_test.go @@ -14,14 +14,13 @@ import ( func testRunnableEnvironment() hub.EnvironmentContext { return hub.EnvironmentContext{ - Env: hub.Env{Alias: "account-1"}, - AccountID: "account-1", - AccountStatus: "active", - AuthorizationStatus: "authorized", - RuntimeID: "runtime-1", - RuntimeNetworkID: "network-1", - Exit: hub.NetworkExit{ID: "exit-1", HealthStatus: "healthy"}, - BindingVersion: 1, + Env: hub.Env{Alias: "account-1"}, + AccountID: "account-1", + AccountStatus: "active", + RuntimeID: "runtime-1", + RuntimeNetworkID: "network-1", + Exit: hub.NetworkExit{ID: "exit-1", HealthStatus: "healthy"}, + BindingVersion: 1, } } diff --git a/internal/controlplane/api/hub_test.go b/internal/controlplane/api/hub_test.go index 6c0950a..cec27e6 100644 --- a/internal/controlplane/api/hub_test.go +++ b/internal/controlplane/api/hub_test.go @@ -67,7 +67,7 @@ func newMemoryStore() *memoryStore { } func TestResumeBlockReasonIsStable(t *testing.T) { - account := accountdomain.Account{AuthorizationStatus: "authorized"} + account := accountdomain.Account{} healthy := hub.EnvironmentContext{Exit: hub.NetworkExit{ID: "exit-a", HealthStatus: "healthy"}} for name, test := range map[string]struct { account accountdomain.Account @@ -75,7 +75,6 @@ func TestResumeBlockReasonIsStable(t *testing.T) { found bool want string }{ - "revoked": {accountdomain.Account{AuthorizationStatus: "revoked"}, healthy, true, "account_revoked"}, "missing binding": {account, hub.EnvironmentContext{}, false, "binding_missing"}, "direct exit": {account, hub.EnvironmentContext{}, true, "account_conflict"}, "unhealthy exit": {account, hub.EnvironmentContext{Exit: hub.NetworkExit{ID: "exit-a", HealthStatus: "unhealthy"}}, true, "network_exit_unhealthy"}, @@ -281,7 +280,7 @@ func (s *memoryStore) CreateBoundEnv(ctx context.Context, env hub.Env, accountID s.mu.Lock() if existing, ok := s.bindings[env.Alias]; ok { if existing.AccountStatus == "" { - existing.AccountStatus, existing.AuthorizationStatus = "active", "authorized" + existing.AccountStatus = "active" } s.mu.Unlock() return existing, false, nil @@ -292,7 +291,7 @@ func (s *memoryStore) CreateBoundEnv(ctx context.Context, env hub.Env, accountID } s.mu.Lock() defer s.mu.Unlock() - bound := hub.EnvironmentContext{Env: env, AccountID: accountID, AccountStatus: "active", AuthorizationStatus: "authorized", + bound := hub.EnvironmentContext{Env: env, AccountID: accountID, AccountStatus: "active", BindingVersion: 1, Exit: s.exits[exitID]} s.bindings[env.Alias] = bound return bound, true, nil @@ -302,7 +301,7 @@ func (s *memoryStore) GetEnvironmentContext(_ context.Context, alias string) (hu defer s.mu.Unlock() if bound, ok := s.bindings[alias]; ok { if bound.AccountStatus == "" { - bound.AccountStatus, bound.AuthorizationStatus = "active", "authorized" + bound.AccountStatus = "active" } return bound, nil } @@ -310,7 +309,7 @@ func (s *memoryStore) GetEnvironmentContext(_ context.Context, alias string) (hu if !ok { return hub.EnvironmentContext{}, hub.ErrNotFound } - return hub.EnvironmentContext{Env: env, AccountID: alias, AccountStatus: "active", AuthorizationStatus: "authorized", + return hub.EnvironmentContext{Env: env, AccountID: alias, AccountStatus: "active", BindingVersion: 1, Exit: s.exits["exit-1"]}, nil } func (s *memoryStore) GetEnvironmentContextForAccount(ctx context.Context, accountID string) (hub.EnvironmentContext, error) { @@ -368,9 +367,9 @@ func (s *memoryStore) ActivateRuntime(_ context.Context, alias, runtimeID string bound = hub.EnvironmentContext{Env: s.envs[alias], AccountID: alias, BindingVersion: 1, Exit: s.exits["exit-1"]} } if bound.AccountStatus == "" { - bound.AccountStatus, bound.AuthorizationStatus = "active", "authorized" + bound.AccountStatus = "active" } - if bound.AccountStatus != "active" || bound.AuthorizationStatus != "authorized" || + if bound.AccountStatus != "active" || bound.BindingVersion != bindingVersion || bound.Exit.ID != exitID || bound.RuntimeCleanupPending { return hub.EnvironmentContext{}, hub.ErrConflict } @@ -1031,8 +1030,7 @@ func TestCreateBrowserDeterministicRejectionDoesNotWedgeBinding(t *testing.T) { func TestReconcileGatewayCreateDoesNotReuseOldNetworkGeneration(t *testing.T) { store := newMemoryStore() store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"} - environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", - AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"], + environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"} store.bindings[environment.Alias] = environment gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ @@ -1106,14 +1104,13 @@ func TestActivationConflictKeepsTheGenerationWonByTheHeartbeat(t *testing.T) { candidateID := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" candidateNetwork := "native-88888888888888888888888888888888" current := hub.EnvironmentContext{ - Env: store.envs["account-a"], - AccountID: "account-a", - AccountStatus: "active", - AuthorizationStatus: "authorized", - BindingVersion: 1, - RuntimeID: candidateID, - RuntimeNetworkID: candidateNetwork, - Exit: store.exits["exit-1"], + Env: store.envs["account-a"], + AccountID: "account-a", + AccountStatus: "active", + BindingVersion: 1, + RuntimeID: candidateID, + RuntimeNetworkID: candidateNetwork, + Exit: store.exits["exit-1"], } store.bindings["account-a"] = current conflict := &activationConflictStore{memoryStore: store, current: current} @@ -1130,8 +1127,7 @@ func TestActivationConflictKeepsTheGenerationWonByTheHeartbeat(t *testing.T) { func TestLegacyActiveRuntimeWithoutNetworkGenerationDoesNotTouchSuccessor(t *testing.T) { store := newMemoryStore() store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"} - environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", - AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"], + environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} store.bindings[environment.Alias] = environment gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ @@ -1567,8 +1563,8 @@ func TestPauseAndRevokeStopContainerWithoutRuntimeLease(t *testing.T) { fixture.gateway.mu.Lock() fixture.gateway.runtimes[0].State = "running" fixture.gateway.mu.Unlock() - if response := do(app, http.MethodPost, "/api/phase-a/accounts/account-a/revoke", ""); response.Code != http.StatusNoContent { - t.Fatalf("revoke running orphan without lease: %d %s", response.Code, response.Body.String()) + if response := do(app, http.MethodPost, "/api/phase-a/accounts/account-a/pause", ""); response.Code != http.StatusNoContent { + t.Fatalf("pause running orphan without lease: %d %s", response.Code, response.Body.String()) } after, err := fixture.store.GetEnvironmentContext(ctx, fixture.bound.Alias) @@ -1576,8 +1572,8 @@ func TestPauseAndRevokeStopContainerWithoutRuntimeLease(t *testing.T) { t.Fatalf("lease-free stop did not converge: %#v err=%v", after, err) } account, err := accountStore.GetAccount(ctx, fixture.bound.AccountID) - if err != nil || account.AuthorizationStatus != "revoked" || account.RuntimeStatus != "paused" { - t.Fatalf("revoke gate did not remain closed: %#v err=%v", account, err) + if err != nil || account.RuntimeStatus != "paused" { + t.Fatalf("pause gate did not apply: %#v err=%v", account, err) } fixture.gateway.mu.Lock() container := fixture.gateway.runtimes[0] @@ -1596,7 +1592,7 @@ func TestPauseAndRevokeStopContainerWithoutRuntimeLease(t *testing.T) { } } if stopCalls != 2 { - t.Fatalf("pause/revoke did not reconcile both lease-free runtimes: %#v", requests) + t.Fatalf("pause did not reconcile both lease-free runtimes: %#v", requests) } } @@ -1874,8 +1870,7 @@ func TestRemoveGatewayRuntimePreservesKnownNetworkGeneration(t *testing.T) { t.Run(test.name, func(t *testing.T) { store := newMemoryStore() store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"} - environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", - AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"], + environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"} store.bindings["account-a"] = environment gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Alias: "account-a", @@ -2670,70 +2665,68 @@ func TestPostgresNonRunnableReconcileAuditsRuntimeRelease(t *testing.T) { t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run PostgreSQL integration coverage") } ctx := context.Background() - for _, authorization := range []string{"authorized", "revoked"} { - for _, observation := range []string{"stopped", "missing"} { - for _, releaseFailure := range []bool{false, true} { - name := authorization + "/" + observation + "/success" - if releaseFailure { - name = authorization + "/" + observation + "/release failure" + for _, observation := range []string{"stopped", "missing"} { + for _, releaseFailure := range []bool{false, true} { + name := observation + "/success" + if releaseFailure { + name = observation + "/release failure" + } + t.Run(name, func(t *testing.T) { + fixture := newPostgresRebindFixture(t, databaseURL) + setFixtureAccountStatus(t, fixture.databaseURL, "active") + var err error + fixture.bound, err = fixture.store.ActivateRuntime(ctx, "account-a", "stopped-container", + fixture.bound.BindingVersion, fixture.bound.Exit.ID, "stopped-network") + if err != nil { + t.Fatal(err) + } + if _, err := fixture.db.ExecContext(ctx, ` + UPDATE social_account SET status = 'paused' WHERE account_id = 'account-a'`); err != nil { + t.Fatal(err) + } + if observation == "stopped" { + fixture.gateway.runtimes = []runtimeStatus{{ + ID: "stopped-container", Alias: "account-a", State: "exited", + BindingVersion: fixture.bound.BindingVersion, NetworkExitID: fixture.bound.Exit.ID, + }} } - t.Run(name, func(t *testing.T) { - fixture := newPostgresRebindFixture(t, databaseURL) - setFixtureAccountStatus(t, fixture.databaseURL, "active") - var err error - fixture.bound, err = fixture.store.ActivateRuntime(ctx, "account-a", "stopped-container", - fixture.bound.BindingVersion, fixture.bound.Exit.ID, "stopped-network") - if err != nil { - t.Fatal(err) - } - if _, err := fixture.db.ExecContext(ctx, ` - UPDATE social_account SET status = 'paused', authorization_status = $1 WHERE account_id = 'account-a'`, authorization); err != nil { - t.Fatal(err) - } - if observation == "stopped" { - fixture.gateway.runtimes = []runtimeStatus{{ - ID: "stopped-container", Alias: "account-a", State: "exited", - BindingVersion: fixture.bound.BindingVersion, NetworkExitID: fixture.bound.Exit.ID, - }} - } - var store HubStore = fixture.store - if releaseFailure { - store = failingRuntimeReleaseStore{Store: fixture.store, err: errors.New("release unavailable")} - } - err = reconcileRuntimeLeases(ctx, store, fakeExitProbe{}, func(hub.NetworkExitAccess) (string, error) { return "", nil }) - wantOutcome, wantReason := "succeeded", "runtime_released" - if releaseFailure { - wantOutcome, wantReason = "failed", "runtime_release_failed" - } - if (err != nil) != releaseFailure { - t.Fatalf("reconcile error=%v, releaseFailure=%v", err, releaseFailure) - } - after, err := fixture.store.GetEnvironmentContext(ctx, "account-a") - if err != nil || (after.RuntimeID != "") == !releaseFailure { - t.Fatalf("runtime lease after reconcile: %#v err=%v", after, err) - } + var store HubStore = fixture.store + if releaseFailure { + store = failingRuntimeReleaseStore{Store: fixture.store, err: errors.New("release unavailable")} + } + err = reconcileRuntimeLeases(ctx, store, fakeExitProbe{}, func(hub.NetworkExitAccess) (string, error) { return "", nil }) + wantOutcome, wantReason := "succeeded", "runtime_released" + if releaseFailure { + wantOutcome, wantReason = "failed", "runtime_release_failed" + } + if (err != nil) != releaseFailure { + t.Fatalf("reconcile error=%v, releaseFailure=%v", err, releaseFailure) + } + after, err := fixture.store.GetEnvironmentContext(ctx, "account-a") + if err != nil || (after.RuntimeID != "") == !releaseFailure { + t.Fatalf("runtime lease after reconcile: %#v err=%v", after, err) + } - var events, operations, missingOperations int - var eventTypes, outcome, reason string - if err := fixture.db.QueryRowContext(ctx, ` + var events, operations, missingOperations int + var eventTypes, outcome, reason string + if err := fixture.db.QueryRowContext(ctx, ` SELECT count(*), count(DISTINCT operation_id), count(*) FILTER (WHERE coalesce(operation_id, '') = ''), string_agg(event_type, ',' ORDER BY id), coalesce(max(outcome) FILTER (WHERE event_type = 'environment_action_finished'), ''), coalesce(max(reason_code) FILTER (WHERE event_type = 'environment_action_finished'), '') FROM audit_event WHERE action = 'reconcile'`).Scan( - &events, &operations, &missingOperations, &eventTypes, &outcome, &reason); err != nil { - t.Fatal(err) - } - if events != 2 || operations != 1 || missingOperations != 0 || - eventTypes != "environment_action_requested,environment_action_finished" || - outcome != wantOutcome || reason != wantReason { - t.Fatalf("reconcile audit mismatch: events=%d operations=%d missing=%d types=%q outcome=%q reason=%q", - events, operations, missingOperations, eventTypes, outcome, reason) - } - }) - } + &events, &operations, &missingOperations, &eventTypes, &outcome, &reason); err != nil { + t.Fatal(err) + } + if events != 2 || operations != 1 || missingOperations != 0 || + eventTypes != "environment_action_requested,environment_action_finished" || + outcome != wantOutcome || reason != wantReason { + t.Fatalf("reconcile audit mismatch: events=%d operations=%d missing=%d types=%q outcome=%q reason=%q", + events, operations, missingOperations, eventTypes, outcome, reason) + } + }) } } } @@ -2761,17 +2754,16 @@ func TestBrowserActionRoutesStartStopAndRejectsUnknown(t *testing.T) { func TestStartRejectsPausedOrRevokedAccountBeforeGatewayCall(t *testing.T) { for _, test := range []struct { - name, status, authorization string + name, status string }{ - {name: "paused", status: "paused", authorization: "authorized"}, - {name: "revoked", status: "paused", authorization: "revoked"}, + {name: "paused", status: "paused"}, } { t.Run(test.name, func(t *testing.T) { store := newMemoryStore() store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}} store.bindings["account-a"] = hub.EnvironmentContext{ Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: test.status, - AuthorizationStatus: test.authorization, BindingVersion: 1, Exit: store.exits["exit-1"], + BindingVersion: 1, Exit: store.exits["exit-1"], } gateway := &fakeGateway{token: "unit-test-gateway-token"} app := newTestApp(t, store, gateway) @@ -2788,12 +2780,12 @@ func TestStartRejectsPausedOrRevokedAccountBeforeGatewayCall(t *testing.T) { } func TestReconcileStopsRuntimeForPausedOrRevokedAccount(t *testing.T) { - for _, authorization := range []string{"authorized", "revoked"} { - t.Run(authorization, func(t *testing.T) { + { + t.Run("paused", func(t *testing.T) { store := newMemoryStore() store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}} store.bindings["account-a"] = hub.EnvironmentContext{ - Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused", AuthorizationStatus: authorization, + Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused", BindingVersion: 1, Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", } @@ -2830,7 +2822,7 @@ func TestActivationConflictCleanupIsGenerationSafe(t *testing.T) { store := newMemoryStore() store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}} environment := hub.EnvironmentContext{ - Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused", AuthorizationStatus: "authorized", + Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused", BindingVersion: 2, Exit: store.exits["exit-1"], } store.bindings[environment.Alias] = environment @@ -2865,7 +2857,7 @@ func TestExplicitStopRejectsStaleBindingBeforeGatewaySideEffect(t *testing.T) { store := newMemoryStore() store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}} store.bindings["account-a"] = hub.EnvironmentContext{ - Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized", + Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 2, Exit: store.exits["exit-1"], RuntimeID: "new-container", } gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ @@ -2890,7 +2882,7 @@ func TestReconcileFinishedAuditUsesRebuiltRuntime(t *testing.T) { store.exits["exit-2"] = hub.NetworkExit{ID: "exit-2", Protocol: "http", Host: "proxy-2.example", Port: 8080, HealthStatus: "healthy", Version: 1} store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}} store.bindings["account-a"] = hub.EnvironmentContext{ - Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized", + Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 2, Exit: store.exits["exit-2"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666", } gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ @@ -2913,7 +2905,7 @@ func TestReconcileContextRefreshFailureClearsAllAuditCorrelation(t *testing.T) { store := newMemoryStore() store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}} store.bindings["account-a"] = hub.EnvironmentContext{ - Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized", + Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 2, Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666", } gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ diff --git a/internal/controlplane/app/app_test.go b/internal/controlplane/app/app_test.go index bc74711..80a5c7a 100644 --- a/internal/controlplane/app/app_test.go +++ b/internal/controlplane/app/app_test.go @@ -366,7 +366,6 @@ func controlPlaneRouteMatrix() []controlPlaneRouteCase { {http.MethodPost, "/api/phase-a/accounts/:id/environment", "/api/phase-a/accounts/missing/environment", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/accounts/:id/start", "/api/phase-a/accounts/missing/start", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/accounts/:id/resume", "/api/phase-a/accounts/missing/resume", "", http.StatusNotFound}, - {http.MethodPost, "/api/phase-a/accounts/:id/revoke", "/api/phase-a/accounts/missing/revoke", "", http.StatusNotFound}, {http.MethodGet, "/api/phase-a/audit", "/api/phase-a/audit", "", http.StatusOK}, } diff --git a/internal/creator/accounts.go b/internal/creator/accounts.go index 360d365..bd7c5d3 100644 --- a/internal/creator/accounts.go +++ b/internal/creator/accounts.go @@ -24,7 +24,7 @@ func (s *Store) EnsureAccountProfile(ctx context.Context, accountID string) erro func accountProfileQuery() string { return ` SELECT a.account_id, a.name, a.platform, a.platform_account_key, - a.authorization_kind, a.authorization_status, a.status, + a.status, p.login_username, p.password_configured, p.real_name_status, p.real_name, p.identity_number, p.note, p.business_status, p.big_account, p.reply_requirements, p.login_status, p.login_reason, @@ -39,7 +39,7 @@ func scanAccountProfile(scanner interface{ Scan(...any) error }) (AccountProfile var checkedAt sql.NullTime if err := scanner.Scan( &result.ID, &result.Name, &result.Platform, &result.PlatformAccountKey, - &result.AuthorizationKind, &result.AuthorizationStatus, &result.RuntimeStatus, + &result.RuntimeStatus, &result.LoginUsername, &result.PasswordConfigured, &result.RealNameStatus, &result.RealName, &result.IdentityNumber, &result.Note, &result.BusinessStatus, &result.BigAccount, &result.ReplyRequirements, &result.LoginStatus, &result.LoginReason, diff --git a/internal/creator/collection.go b/internal/creator/collection.go index 3607277..0062e37 100644 --- a/internal/creator/collection.go +++ b/internal/creator/collection.go @@ -340,7 +340,7 @@ func (s *Store) ListDueOwnedAccounts(ctx context.Context, now time.Time, interva ON works_checkpoint.source_type='owned' AND works_checkpoint.source_id=account.account_id AND works_checkpoint.collection_kind='works' LEFT JOIN creator_collection_checkpoint comments_checkpoint ON comments_checkpoint.source_type='owned' AND comments_checkpoint.source_id=account.account_id AND comments_checkpoint.collection_kind='comments' - WHERE account.platform = 'douyin' AND account.authorization_status='authorized' + WHERE account.platform = 'douyin' AND profile.login_status='logged_in' AND COALESCE(works_checkpoint.status, '') <> 'blocked' AND COALESCE(comments_checkpoint.status, '') <> 'blocked' diff --git a/internal/creator/logic.go b/internal/creator/logic.go index 12f9bdc..d553028 100644 --- a/internal/creator/logic.go +++ b/internal/creator/logic.go @@ -222,7 +222,7 @@ func CanWrite(profile AccountProfile, automatic bool, action string) error { return ErrConflict } } - if profile.LoginStatus != "logged_in" || profile.AuthorizationStatus != "authorized" { + if profile.LoginStatus != "logged_in" { return ErrConflict } return nil diff --git a/internal/creator/logic_test.go b/internal/creator/logic_test.go index edcac73..98468a5 100644 --- a/internal/creator/logic_test.go +++ b/internal/creator/logic_test.go @@ -69,7 +69,7 @@ func TestActionTargetAndBusinessStatusGates(t *testing.T) { if ActionTargetValid(ActionReplyComment, "uid-1", "", "", "comment") { t.Fatal("reply without a comment target must be blocked") } - profile := AccountProfile{AuthorizationStatus: "authorized", LoginStatus: "logged_in", BusinessStatus: "muted"} + profile := AccountProfile{LoginStatus: "logged_in", BusinessStatus: "muted"} if err := CanWrite(profile, false, ActionDM); err == nil { t.Fatal("muted accounts cannot manually send DMs") } diff --git a/internal/creator/models.go b/internal/creator/models.go index 3828d2b..5c5bc1a 100644 --- a/internal/creator/models.go +++ b/internal/creator/models.go @@ -52,27 +52,25 @@ type SettingsUpdate struct { } type AccountProfile struct { - ID string `json:"id"` - Name string `json:"name"` - Platform string `json:"platform"` - PlatformAccountKey string `json:"platform_account_key"` - AuthorizationKind string `json:"authorization_kind"` - AuthorizationStatus string `json:"authorization_status"` - RuntimeStatus string `json:"runtime_status"` - LoginUsername string `json:"login_username"` - PasswordConfigured bool `json:"password_configured"` - RealNameStatus string `json:"real_name_status"` - RealName string `json:"real_name"` - IdentityNumber string `json:"identity_number"` - Note string `json:"note"` - BusinessStatus string `json:"business_status"` - BigAccount bool `json:"big_account"` - ReplyRequirements string `json:"reply_requirements"` - LoginStatus string `json:"login_status"` - LoginReason string `json:"login_reason"` - LoginCheckedAt *time.Time `json:"login_checked_at,omitempty"` - CooldownSeconds int64 `json:"cooldown_seconds"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + PlatformAccountKey string `json:"platform_account_key"` + RuntimeStatus string `json:"runtime_status"` + LoginUsername string `json:"login_username"` + PasswordConfigured bool `json:"password_configured"` + RealNameStatus string `json:"real_name_status"` + RealName string `json:"real_name"` + IdentityNumber string `json:"identity_number"` + Note string `json:"note"` + BusinessStatus string `json:"business_status"` + BigAccount bool `json:"big_account"` + ReplyRequirements string `json:"reply_requirements"` + LoginStatus string `json:"login_status"` + LoginReason string `json:"login_reason"` + LoginCheckedAt *time.Time `json:"login_checked_at,omitempty"` + CooldownSeconds int64 `json:"cooldown_seconds"` + UpdatedAt time.Time `json:"updated_at"` } type AccountProfileUpdate struct { diff --git a/internal/environment/environment.go b/internal/environment/environment.go index 4780608..a905548 100644 --- a/internal/environment/environment.go +++ b/internal/environment/environment.go @@ -49,7 +49,6 @@ type EnvironmentContext struct { Env AccountID string `json:"account_id"` AccountStatus string `json:"account_status"` - AuthorizationStatus string `json:"authorization_status"` BindingVersion int64 `json:"binding_version"` RuntimeCleanupPending bool `json:"runtime_cleanup_pending,omitempty"` RuntimeCleanupBindingVersion int64 `json:"runtime_cleanup_binding_version,omitempty"` @@ -432,7 +431,6 @@ func (s *Store) CreateBoundEnv(ctx context.Context, env Env, accountID, exitID s JOIN gateway ON gateway.name = $3 LEFT JOIN network_exit network ON network.exit_id = NULLIF($6, '') WHERE account.account_id = $5 AND account.status = 'paused' - AND account.authorization_status = 'authorized' AND ($6 = '' OR network.health_status = 'healthy') RETURNING alias`, env.Alias, env.Name, env.Gateway, encoded, accountID, exitID).Scan(&created); err != nil { return EnvironmentContext{}, false, rowError(err) @@ -464,7 +462,7 @@ func (s *Store) GetEnvironmentContext(ctx context.Context, alias string) (Enviro var cleanupBindingVersion sql.NullInt64 err = tx.QueryRowContext(ctx, ` SELECT environment.alias, environment.name, gateway.name, - environment.fingerprint, environment.created_at, account.account_id, account.status, account.authorization_status, + environment.fingerprint, environment.created_at, account.account_id, account.status, environment.version, environment.runtime_cleanup_pending, environment.runtime_cleanup_binding_version, environment.runtime_cleanup_runtime_id, environment.runtime_cleanup_network_id, @@ -481,7 +479,7 @@ func (s *Store) GetEnvironmentContext(ctx context.Context, alias string) (Enviro LEFT JOIN network_exit network ON network.id = environment.exit_id WHERE environment.alias = $1`, alias). Scan(&result.Alias, &result.Name, &result.Gateway, &encoded, &result.CreatedAt, - &result.AccountID, &result.AccountStatus, &result.AuthorizationStatus, + &result.AccountID, &result.AccountStatus, &result.BindingVersion, &result.RuntimeCleanupPending, &cleanupBindingVersion, &cleanupRuntimeID, &cleanupNetworkID, &result.Exit.ID, &result.Exit.Protocol, &result.Exit.Host, &result.Exit.Port, @@ -584,7 +582,6 @@ func validateEnvironmentRebind(ctx context.Context, tx *sql.Tx, alias, exitID st FROM browser_env environment JOIN social_account account ON account.id = environment.account_id WHERE environment.alias = $1 AND account.status = 'paused' - AND account.authorization_status = 'authorized' AND NOT environment.runtime_cleanup_pending FOR UPDATE OF environment, account`, alias).Scan(&accountID, &bindingVersion) if errors.Is(err, sql.ErrNoRows) { @@ -688,21 +685,21 @@ func (s *Store) ActivateRuntime(ctx context.Context, alias, runtimeID string, bi return EnvironmentContext{}, errors.New("begin runtime activation") } defer tx.Rollback() - var accountID, currentExitID, accountStatus, authorizationStatus string + var accountID, currentExitID, accountStatus string var currentBindingVersion int64 var cleanupPending bool err = tx.QueryRowContext(ctx, ` SELECT account.account_id, environment.version, COALESCE(network.exit_id, ''), environment.runtime_cleanup_pending, - account.status, account.authorization_status + account.status FROM browser_env environment JOIN social_account account ON account.id = environment.account_id LEFT JOIN network_exit network ON network.id = environment.exit_id WHERE environment.alias = $1 FOR UPDATE OF environment, account`, alias). - Scan(&accountID, ¤tBindingVersion, ¤tExitID, &cleanupPending, &accountStatus, &authorizationStatus) + Scan(&accountID, ¤tBindingVersion, ¤tExitID, &cleanupPending, &accountStatus) if err != nil { return EnvironmentContext{}, rowError(err) } - if cleanupPending || accountStatus != "active" || authorizationStatus != "authorized" || + if cleanupPending || accountStatus != "active" || currentBindingVersion != bindingVersion || currentExitID != exitID { return EnvironmentContext{}, ErrConflict } diff --git a/internal/environment/migration043_probe_test.go b/internal/environment/migration043_probe_test.go index c2e09fd..77aebc6 100644 --- a/internal/environment/migration043_probe_test.go +++ b/internal/environment/migration043_probe_test.go @@ -70,6 +70,6 @@ func TestMigration043ConsolidatedSchemaShape(t *testing.T) { // audit_event 任务列已删 assertDatabaseCount(t, db, `SELECT count(*) FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'audit_event' AND column_name IN ('confirmation_id','confirmation_version','attempt_id','task_id','runtime_instance_id')`, 0) - // 统一登记表:1-38(除 36)、1017-1044、43 全部登记 - assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 51) + // 统一登记表:1-38(除 36)、1017-1045、43 全部登记 + assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 52) } diff --git a/internal/environment/migration_test.go b/internal/environment/migration_test.go index b6e8f9d..a44e30a 100644 --- a/internal/environment/migration_test.go +++ b/internal/environment/migration_test.go @@ -30,7 +30,7 @@ func TestUnifiedAccountMigration(t *testing.T) { t.Fatal(err) } defer db.Close() - assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 51) + assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 52) assertDatabaseCount(t, db, `SELECT count(*) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name IN ('social_account', 'browser_env', 'network_exit', 'environment_binding')`, 3) assertDatabaseCount(t, db, `SELECT count(*) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = 'browser_image'`, 0) @@ -42,7 +42,7 @@ func TestUnifiedAccountMigration(t *testing.T) { store = openFullyMigratedHub(t, ctx, testURL) store.Close() - assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 51) + assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 52) }) t.Run("legacy migration 013 without account secrets is repaired forward", func(t *testing.T) { @@ -129,13 +129,13 @@ func TestUnifiedAccountMigration(t *testing.T) { assertDatabaseCount(t, db, `SELECT count(*) FROM audit_event`, 0) assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration WHERE version IN (3, 4, 5, 6, 7, 8, 9)`, 7) if _, err := db.Exec(` - INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status) - VALUES ('mapped', 'os_keyring', 'creatorhub/mapped', 'mock', 'mapped', 'owned', 'authorized')`); err != nil { + INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key) + VALUES ('mapped', 'os_keyring', 'creatorhub/mapped', 'mock', 'mapped')`); err != nil { t.Fatal(err) } if _, err := db.Exec(` - INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status) - VALUES ('duplicate', 'os_keyring', 'creatorhub/duplicate', 'mock', 'mapped', 'owned', 'authorized')`); err == nil { + INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key) + VALUES ('duplicate', 'os_keyring', 'creatorhub/duplicate', 'mock', 'mapped')`); err == nil { t.Fatal("duplicate platform account must fail") } }) diff --git a/internal/environment/migrations/1045_drop_authorization_status.sql b/internal/environment/migrations/1045_drop_authorization_status.sql new file mode 100644 index 0000000..8460213 --- /dev/null +++ b/internal/environment/migrations/1045_drop_authorization_status.sql @@ -0,0 +1,6 @@ +-- 移除「授权状态/授权种类」概念:产品已收敛为自有账号管理,前端无撤销入口, +-- authorization_status 恒为 'authorized'、authorization_kind 恒为 'owned',列与判定全链路删除; +-- 账号有效性由 status(active/paused)单一状态机表达。 +ALTER TABLE social_account DROP COLUMN IF EXISTS authorization_status; +ALTER TABLE social_account DROP COLUMN IF EXISTS revoked_at; +ALTER TABLE social_account DROP COLUMN IF EXISTS authorization_kind; diff --git a/internal/environment/store.go b/internal/environment/store.go index bcfe6d4..a727e69 100644 --- a/internal/environment/store.go +++ b/internal/environment/store.go @@ -173,6 +173,9 @@ var migration1043 string //go:embed migrations/1044_creator_account_metric_friend_count.sql var migration1044 string +//go:embed migrations/1045_drop_authorization_status.sql +var migration1045 string + var ( ErrConflict = errors.New("resource conflicts with existing state") ErrInvalid = errors.New("invalid environment input") @@ -312,7 +315,7 @@ func (s *Store) migrate(ctx context.Context) error { {1023, migration1023}, {1024, migration1024}, {1025, migration1025}, {1026, migration1026}, {1027, migration1027}, {1028, migration1028}, {1029, migration1029}, {1030, migration1030}, {1031, migration1031}, {1032, migration1032}, {1033, migration1033}, {1034, migration1034}, {1035, migration1035}, {1036, migration1036}, {1037, migration1037}, {1038, migration1038}, {1039, migration1039}, {1040, migration1040}, - {1041, migration1041}, {1042, migration1042}, {1043, migration1043}, {1044, migration1044}, + {1041, migration1041}, {1042, migration1042}, {1043, migration1043}, {1044, migration1044}, {1045, migration1045}, {43, migration043}, {44, migration044}} { var applied bool if err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM schema_migration WHERE version = $1)`, migration.version).Scan(&applied); err != nil { diff --git a/internal/environment/store_test.go b/internal/environment/store_test.go index 39d73ae..3b6fb78 100644 --- a/internal/environment/store_test.go +++ b/internal/environment/store_test.go @@ -293,9 +293,9 @@ func TestFingerprintSeedIsGloballyUnique(t *testing.T) { t.Fatal(err) } if _, err := store.db.ExecContext(ctx, ` - INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status) - VALUES ('seed-account-a', 'os_keyring', 'creatorhub/seed-a', 'mock', 'seed-account-a', 'owned', 'authorized'), - ('seed-account-b', 'os_keyring', 'creatorhub/seed-b', 'mock', 'seed-account-b', 'owned', 'authorized')`); err != nil { + INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key) + VALUES ('seed-account-a', 'os_keyring', 'creatorhub/seed-a', 'mock', 'seed-account-a'), + ('seed-account-b', 'os_keyring', 'creatorhub/seed-b', 'mock', 'seed-account-b')`); err != nil { t.Fatal(err) } env := Env{Alias: "seed-environment-a", Name: "Seed A", Gateway: "gw-seed"} @@ -355,8 +355,8 @@ func TestHubWorkflow(t *testing.T) { } if _, err := store.db.ExecContext(ctx, ` - INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status) - VALUES ('shop-owner', 'os_keyring', 'creatorhub/shop-owner', 'mock', 'shop-owner', 'owned', 'authorized')`); err != nil { + INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key) + VALUES ('shop-owner', 'os_keyring', 'creatorhub/shop-owner', 'mock', 'shop-owner')`); err != nil { t.Fatal(err) } env := Env{ @@ -370,8 +370,8 @@ func TestHubWorkflow(t *testing.T) { t.Fatalf("expected idempotent environment reuse, created=%v err=%v", created, err) } if _, err := store.db.ExecContext(ctx, ` - INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status) - VALUES ('shop-owner-2', 'os_keyring', 'creatorhub/shop-owner-2', 'mock', 'shop-owner-2', 'owned', 'authorized')`); err != nil { + INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key) + VALUES ('shop-owner-2', 'os_keyring', 'creatorhub/shop-owner-2', 'mock', 'shop-owner-2')`); err != nil { t.Fatal(err) } if _, _, err := store.CreateBoundEnv(ctx, Env{Alias: "shop-02", Name: "店铺二号", Gateway: "missing", Fingerprint: Fingerprint{Seed: 2}}, "shop-owner-2", ""); !errors.Is(err, ErrNotFound) { @@ -437,8 +437,8 @@ func TestNetworkExitBindingRuntimeAndAuditWorkflow(t *testing.T) { } if _, err := store.db.ExecContext(ctx, ` INSERT INTO social_account - (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status) - VALUES ('account-a', 'os_keyring', 'creatorhub/account-a', 'mock', 'account-a', 'owned', 'authorized')`); err != nil { + (account_id, credential_provider, credential_key, platform, platform_account_key) + VALUES ('account-a', 'os_keyring', 'creatorhub/account-a', 'mock', 'account-a')`); err != nil { t.Fatal(err) } if _, err := store.CreateGateway(ctx, "gw-main", "http://127.0.0.1:8081", "unit-test-gateway-token"); err != nil { @@ -502,8 +502,8 @@ func TestNetworkExitBindingRuntimeAndAuditWorkflow(t *testing.T) { } if _, err := store.db.ExecContext(ctx, ` INSERT INTO social_account - (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status) - VALUES ('account-b', 'os_keyring', 'creatorhub/account-b', 'mock', 'account-b', 'owned', 'authorized'); + (account_id, credential_provider, credential_key, platform, platform_account_key) + VALUES ('account-b', 'os_keyring', 'creatorhub/account-b', 'mock', 'account-b'); INSERT INTO browser_env (alias, name, gateway_id, fingerprint, account_id) VALUES ('environment-b', '环境 B', (SELECT g.id FROM gateway g WHERE g.name = 'gw-main'), '{"seed":2}', (SELECT a.id FROM social_account a WHERE a.account_id = 'account-b'))`); err != nil { @@ -692,8 +692,8 @@ func TestRuntimeCleanupFenceAndBoundCreateReadiness(t *testing.T) { store := openFullyMigratedHub(t, ctx, isolatedDatabaseURL(t, databaseURL)) t.Cleanup(func() { _ = store.Close() }) if _, err := store.db.ExecContext(ctx, ` - INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status) - VALUES ('fence-a', 'os_keyring', 'creatorhub/fence-a', 'mock', 'fence-a', 'owned', 'authorized')`); err != nil { + INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key) + VALUES ('fence-a', 'os_keyring', 'creatorhub/fence-a', 'mock', 'fence-a')`); err != nil { t.Fatal(err) } if _, err := store.CreateGateway(ctx, "gw-fence", "http://127.0.0.1:8081", "unit-test-gateway-token"); err != nil { diff --git a/web/src/components/accounts/AccountManagementList.tsx b/web/src/components/accounts/AccountManagementList.tsx index dae89a8..1306eed 100644 --- a/web/src/components/accounts/AccountManagementList.tsx +++ b/web/src/components/accounts/AccountManagementList.tsx @@ -19,7 +19,6 @@ interface Row { platform_account_key: string; tags?: string[]; runtime_status?: string; - authorization_status?: string; work_count?: number; latest_published_at?: string | null; follower_count?: number | null; @@ -127,7 +126,7 @@ export default function AccountManagementList() { } } - // 扫码后核验:成功视为已登录,刷新列表拿到最新 authorization_status。 + // 扫码后核验:成功视为已登录,刷新列表同步最新账号状态。 async function verifyLogin() { if (!loginTarget) return; setLoginBusy(true); @@ -212,12 +211,9 @@ export default function AccountManagementList() { title: '状态', dataIndex: 'status', render: (_, account) => ( - - - {account.runtime_status === 'active' ? '启用' : '暂停'} - - {account.authorization_status === 'authorized' ? '已授权' : '已撤销'} - + + {account.runtime_status === 'active' ? '启用' : '暂停'} + ) }, { title: '操作', diff --git a/web/src/pages/accounts/$id/index.tsx b/web/src/pages/accounts/$id/index.tsx index 8751155..4e77f57 100644 --- a/web/src/pages/accounts/$id/index.tsx +++ b/web/src/pages/accounts/$id/index.tsx @@ -209,7 +209,6 @@ export default function Page() { - {account.authorization_status === 'authorized' ? '已授权' : '已撤销'} {`${account.runtime_status === 'active' ? '启用' : '暂停'} · 版本 ${account.version}`} {account.tags?.join('、') || '无'} diff --git a/web/src/utils/helpers.ts b/web/src/utils/helpers.ts index c0d930d..11838ee 100644 --- a/web/src/utils/helpers.ts +++ b/web/src/utils/helpers.ts @@ -58,9 +58,8 @@ export function extractShareURL(value?: string): string { return candidates?.find((candidate) => platformForShareURL(candidate)) || candidates?.[0] || ''; } -// 账号可恢复性判定(对齐归档版 accountReadiness):授权、绑定、出口健康、账号暂停四层。 +// 账号可恢复性判定(对齐归档版 accountReadiness):绑定、出口健康、账号暂停三层。 export const readinessReasonText: Record = { - account_revoked: '授权已撤销', account_paused: '账号已暂停', binding_missing: '未绑定运行环境', network_exit_missing: '未绑定固定出口', @@ -80,8 +79,6 @@ export interface AccountReadiness { } export function accountReadiness(account: any, binding: any, bindingError: any = null): AccountReadiness { - if (account.authorization_status !== 'authorized') - return { label: '授权已撤销', reason: 'account_revoked', canResume: false, ready: false }; if (bindingError) return { label: '环境状态未知', reason: 'environment_unavailable', canResume: false, ready: false }; if (!binding) return { label: '未绑定运行环境', reason: 'binding_missing', canResume: false, ready: false }; let blocked = ''; @@ -105,9 +102,5 @@ export function accountReadiness(account: any, binding: any, bindingError: any = } export function isCollectionAccount(account: any): boolean { - return ( - account.authorization_status === 'authorized' && - ['normal', 'muted'].includes(account.business_status) && - account.login_status === 'logged_in' - ); + return ['normal', 'muted'].includes(account.business_status) && account.login_status === 'logged_in'; }