fix(account): close create modal on success and make cookies optional

- AccountList.createAccount 成功后关闭创建弹窗(此前只提示不关闭)
- Cookies 前端改为可选:留空代表扫码登录场景,提交时不携带该字段
- 后端 validAccount 允许空凭据(此前 http.ParseCookie("") 直接拒绝)
- CreateAccount 空凭据时跳过 keyring 写入,不产生空值记录;回滚补偿仅在实际写入过凭据时执行

验证:go test ./... / go vet ./... / 双端构建通过;web vitest 66 用例通过(新增无 cookies 创建+弹窗关闭用例);npm run build 通过
This commit is contained in:
2026-09-02 17:00:19 +08:00
parent f921a2e835
commit 1e92e16ed3
4 changed files with 872 additions and 283 deletions
+18 -8
View File
@@ -273,15 +273,20 @@ func (s *Store) CreateAccount(ctx context.Context, account Account, credentials
if !validAccount(account) || credentials == nil {
return ErrInvalid
}
if err := credentials.Store(ctx, account.CredentialReference, account.CredentialKey, account.Cookies); err != nil {
storeErr := errors.New("store account credential")
if cleanupErr := credentials.Delete(context.WithoutCancel(ctx), account.CredentialReference, account.CredentialKey); cleanupErr != nil {
storeErr = errors.Join(storeErr, errors.New("delete incomplete account credential"))
// 空凭据(扫码登录场景)不写 keyring:凭据留待后续登录/同步链路补齐
stored := false
if account.Cookies != "" {
if err := credentials.Store(ctx, account.CredentialReference, account.CredentialKey, account.Cookies); err != nil {
storeErr := errors.New("store account credential")
if cleanupErr := credentials.Delete(context.WithoutCancel(ctx), account.CredentialReference, account.CredentialKey); cleanupErr != nil {
storeErr = errors.Join(storeErr, errors.New("delete incomplete account credential"))
}
return storeErr
}
return storeErr
stored = true
}
defer func() {
if err != nil && !errors.Is(err, ErrAccountCreationUnknown) {
if stored && err != nil && !errors.Is(err, ErrAccountCreationUnknown) {
if cleanupErr := credentials.Delete(context.WithoutCancel(ctx), account.CredentialReference, account.CredentialKey); cleanupErr != nil {
err = errors.Join(err, errors.New("delete orphaned account credential"))
}
@@ -393,8 +398,13 @@ func validAccount(account Account) bool {
return false
}
}
_, err := http.ParseCookie(account.Cookies)
return err == nil
// 空凭据合法(扫码登录场景);非空时才校验 Cookie Header 格式
if account.Cookies != "" {
if _, err := http.ParseCookie(account.Cookies); err != nil {
return false
}
}
return true
}
func (s *Store) CreateDraft(ctx context.Context, draft Draft) error {
+37
View File
@@ -79,6 +79,11 @@ func TestValidationRejectsInvalidInputsBeforePersistence(t *testing.T) {
t.Fatalf("supported platform rejected: %s", platform)
}
}
empty := valid
empty.Cookies = ""
if !validAccount(empty) {
t.Fatal("empty cookies must stay valid (scan-to-login account)")
}
for name, mutate := range map[string]func(*Account){
"id": func(account *Account) { account.ID = "INVALID" },
"name": func(account *Account) { account.Name = " " },
@@ -725,6 +730,38 @@ func TestPhaseAOfflineWorkflow(t *testing.T) {
}
}
func TestCreateAccountWithoutCookiesSkipsCredentialStore(t *testing.T) {
databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL")
if databaseURL == "" {
t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run PostgreSQL integration coverage")
}
ctx := context.Background()
store, err := Open(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = store.Close() })
applyHubMigrationsForPhaseATest(t, store)
if _, err := store.db.ExecContext(ctx, `
TRUNCATE audit_event, execution_attempt, operation_task, confirmation, content_draft,
runtime_instance, environment_binding, network_exit, social_account, credential_reference,
browser_env, browser_image, gateway RESTART IDENTITY CASCADE`); err != nil {
t.Fatal(err)
}
credentials := &testCredentialBridge{values: map[string]string{}}
account := Account{ID: "account-no-cookies", Name: "扫码账号", Platform: "douyin", PlatformAccountKey: "qr-login",
Tags: []string{}, Cookies: "",
CredentialReference: CredentialReference{ID: "account-no-cookies-cookies", Provider: "os_keyring"}, CredentialKey: "creatorhub/account-no-cookies/cookies"}
if err := store.CreateAccount(ctx, account, credentials); err != nil {
t.Fatalf("creating an account without cookies failed: %v", err)
}
if _, stored := credentials.values[account.CredentialKey]; stored {
t.Fatal("empty cookies must not be written to the credential provider")
}
assertCount(t, store, `SELECT count(*) FROM social_account WHERE id = $1`, 1, account.ID)
assertCount(t, store, `SELECT count(*) FROM credential_reference WHERE id = $1`, 1, account.CredentialReference.ID)
}
func TestAccountCredentialCommitResult(t *testing.T) {
databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL")
if databaseURL == "" {