141 lines
5.1 KiB
Go
141 lines
5.1 KiB
Go
package account
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
// CheckAccountDeletion verifies that deleting an account will not interrupt an active run.
|
|
func (s *Store) CheckAccountDeletion(ctx context.Context, accountID string) error {
|
|
if !idPattern.MatchString(accountID) {
|
|
return ErrInvalid
|
|
}
|
|
var exists bool
|
|
if err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM social_account WHERE id = $1)`, accountID).Scan(&exists); err != nil {
|
|
return errors.New("check account deletion state")
|
|
}
|
|
if !exists {
|
|
return ErrNotFound
|
|
}
|
|
var active bool
|
|
if err := s.db.QueryRowContext(ctx, `
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM runtime_instance
|
|
WHERE account_id = $1 AND released_at IS NULL
|
|
UNION ALL
|
|
SELECT 1 FROM operation_task
|
|
WHERE account_id = $1 AND state = 'executing'
|
|
)`, accountID).Scan(&active); err != nil {
|
|
return errors.New("check account deletion state")
|
|
}
|
|
if active {
|
|
return ErrConflict
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteAccountData removes Phase A data while keeping the account row for the final deletion step.
|
|
func (s *Store) DeleteAccountData(ctx context.Context, accountID string) error {
|
|
if !idPattern.MatchString(accountID) {
|
|
return ErrInvalid
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return errors.New("begin account deletion transaction")
|
|
}
|
|
defer tx.Rollback()
|
|
var lockedID string
|
|
if err := tx.QueryRowContext(ctx, `SELECT id FROM social_account WHERE id = $1 FOR UPDATE`, accountID).Scan(&lockedID); err != nil {
|
|
return rowError(err)
|
|
}
|
|
var active bool
|
|
if err := tx.QueryRowContext(ctx, `
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM runtime_instance
|
|
WHERE account_id = $1 AND released_at IS NULL
|
|
UNION ALL
|
|
SELECT 1 FROM operation_task
|
|
WHERE account_id = $1 AND state = 'executing'
|
|
)`, accountID).Scan(&active); err != nil {
|
|
return errors.New("check account deletion state")
|
|
}
|
|
if active {
|
|
return ErrConflict
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `SELECT set_config('creatorhub.account_deletion', 'on', true)`); err != nil {
|
|
return errors.New("enable account deletion audit cleanup")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `
|
|
DELETE FROM audit_event
|
|
WHERE account_id = $1
|
|
OR confirmation_id IN (SELECT id FROM confirmation WHERE account_id = $1)
|
|
OR task_id IN (SELECT id FROM operation_task WHERE account_id = $1)
|
|
OR attempt_id IN (
|
|
SELECT id FROM execution_attempt
|
|
WHERE task_id IN (SELECT id FROM operation_task WHERE account_id = $1)
|
|
)
|
|
OR browser_env_alias IN (
|
|
SELECT browser_env_alias FROM environment_binding WHERE account_id = $1
|
|
)
|
|
OR runtime_instance_id IN (SELECT id FROM runtime_instance WHERE account_id = $1)`, accountID); err != nil {
|
|
return errors.New("delete account audit data")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `UPDATE operation_task SET current_attempt_id = NULL WHERE account_id = $1`, accountID); err != nil {
|
|
return errors.New("detach account task attempts")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `
|
|
DELETE FROM execution_attempt
|
|
WHERE task_id IN (SELECT id FROM operation_task WHERE account_id = $1)`, accountID); err != nil {
|
|
return errors.New("delete account task attempts")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM operation_task WHERE account_id = $1`, accountID); err != nil {
|
|
return errors.New("delete account tasks")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM confirmation WHERE account_id = $1`, accountID); err != nil {
|
|
return errors.New("delete account confirmations")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM content_draft WHERE account_id = $1`, accountID); err != nil {
|
|
return errors.New("delete account drafts")
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM runtime_instance WHERE account_id = $1`, accountID); err != nil {
|
|
return errors.New("delete account runtime records")
|
|
}
|
|
return commit(tx)
|
|
}
|
|
|
|
// DeleteAccount removes the account row and its external cookie credential.
|
|
// Call DeleteAccountData and delete account-owned creator/environment data first.
|
|
func (s *Store) DeleteAccount(ctx context.Context, accountID string, credentials CredentialBridge) error {
|
|
if !idPattern.MatchString(accountID) || credentials == nil {
|
|
return ErrInvalid
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return errors.New("begin account record deletion")
|
|
}
|
|
defer tx.Rollback()
|
|
var reference CredentialReference
|
|
var key string
|
|
if err := tx.QueryRowContext(ctx, `
|
|
SELECT credential.id, credential.provider, credential.reference_key
|
|
FROM social_account account
|
|
JOIN credential_reference credential ON credential.id = account.credential_reference_id
|
|
WHERE account.id = $1
|
|
FOR UPDATE`, accountID).Scan(&reference.ID, &reference.Provider, &key); err != nil {
|
|
return rowError(err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM social_account WHERE id = $1`, accountID); err != nil {
|
|
return publicDatabaseError(err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM credential_reference WHERE id = $1`, reference.ID); err != nil {
|
|
return publicDatabaseError(err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return errors.New("commit account record deletion")
|
|
}
|
|
if err := credentials.Delete(context.WithoutCancel(ctx), reference, key); err != nil {
|
|
return errors.Join(errors.New("delete account credential"), err)
|
|
}
|
|
return nil
|
|
}
|