refactor(account): generalize RemoveEmailIdentity to RemoveIdentity(kind)

Generalize the email-erase store op to any identity kind (with the same
last-identity guard and, for email, the pending-confirmation cleanup) so the
profile Unlink control can reuse it for Telegram/VK. RemoveEmailIdentity stays as a
thin wrapper for the admin console.
This commit is contained in:
Ilia Denisov
2026-07-03 09:20:59 +02:00
parent 3a823ca7ef
commit a3eb4719de
+25 -17
View File
@@ -21,25 +21,25 @@ var ErrIdentityTaken = errors.New("account: identity already linked to another a
// none, making it unreachable after logout. The admin email-erase refuses it. // none, making it unreachable after logout. The admin email-erase refuses it.
var ErrLastIdentity = errors.New("account: cannot remove the last identity") var ErrLastIdentity = errors.New("account: cannot remove the last identity")
// RemoveEmailIdentity deletes the account's email identity and any pending confirmations // RemoveIdentity deletes the account's identity of the given kind (and, for an email,
// for it, freeing the address for reuse. It refuses when the email is the account's only // any pending confirmations for it), freeing it for reuse. It refuses when that is the
// identity (ErrLastIdentity) — that would leave the account unreachable — and returns // account's only identity (ErrLastIdentity) — which would leave the account
// ErrNotFound when the account has no email identity. It backs the admin console's // unreachable — and returns ErrNotFound when the account has no identity of that kind.
// "erase email" action. // It backs the profile Unlink control and the admin "erase email" action.
func (s *Store) RemoveEmailIdentity(ctx context.Context, accountID uuid.UUID) error { func (s *Store) RemoveIdentity(ctx context.Context, accountID uuid.UUID, kind string) error {
ids, err := s.Identities(ctx, accountID) ids, err := s.Identities(ctx, accountID)
if err != nil { if err != nil {
return err return err
} }
hasEmail, others := false, 0 hasKind, others := false, 0
for _, id := range ids { for _, id := range ids {
if id.Kind == KindEmail { if id.Kind == kind {
hasEmail = true hasKind = true
} else { } else {
others++ others++
} }
} }
if !hasEmail { if !hasKind {
return ErrNotFound return ErrNotFound
} }
if others == 0 { if others == 0 {
@@ -48,21 +48,29 @@ func (s *Store) RemoveEmailIdentity(ctx context.Context, accountID uuid.UUID) er
return withTx(ctx, s.db, func(tx *sql.Tx) error { return withTx(ctx, s.db, func(tx *sql.Tx) error {
delID := table.Identities.DELETE().WHERE( delID := table.Identities.DELETE().WHERE(
table.Identities.AccountID.EQ(postgres.UUID(accountID)). table.Identities.AccountID.EQ(postgres.UUID(accountID)).
AND(table.Identities.Kind.EQ(postgres.String(KindEmail))), AND(table.Identities.Kind.EQ(postgres.String(kind))),
) )
if _, err := delID.ExecContext(ctx, tx); err != nil { if _, err := delID.ExecContext(ctx, tx); err != nil {
return fmt.Errorf("account: delete email identity %s: %w", accountID, err) return fmt.Errorf("account: delete %s identity %s: %w", kind, accountID, err)
} }
delConf := table.EmailConfirmations.DELETE().WHERE( if kind == KindEmail {
table.EmailConfirmations.AccountID.EQ(postgres.UUID(accountID)), delConf := table.EmailConfirmations.DELETE().WHERE(
) table.EmailConfirmations.AccountID.EQ(postgres.UUID(accountID)),
if _, err := delConf.ExecContext(ctx, tx); err != nil { )
return fmt.Errorf("account: delete email confirmations %s: %w", accountID, err) if _, err := delConf.ExecContext(ctx, tx); err != nil {
return fmt.Errorf("account: delete email confirmations %s: %w", accountID, err)
}
} }
return nil return nil
}) })
} }
// RemoveEmailIdentity erases the account's email identity. It backs the admin console's
// "erase email" action; the user-facing profile never unlinks email (it is changed).
func (s *Store) RemoveEmailIdentity(ctx context.Context, accountID uuid.UUID) error {
return s.RemoveIdentity(ctx, accountID, KindEmail)
}
// RequestLinkCode issues and mails a confirm-code for email to accountID, // RequestLinkCode issues and mails a confirm-code for email to accountID,
// replacing any prior pending code. Unlike RequestCode it never refuses up front // replacing any prior pending code. Unlike RequestCode it never refuses up front
// (taken or already-confirmed): possession of the address is the authorization for // (taken or already-confirmed): possession of the address is the authorization for