feat(account): journal detached credentials to the retention log
On unlink (RemoveIdentity, reason=unlink) and email change (replaceEmailIdentity, reason=change) write the outgoing credential to retained_identities before removing the live identities row — so the legal dossier survives while the (kind, external_id) frees for reuse. Same transaction, so the dossier and live state cannot diverge. Integration tests cover both reasons.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"scrabble/backend/internal/postgres/jet/backend/table"
|
||||
)
|
||||
|
||||
// Reasons recorded on a retained_identities row: what detached the credential from its
|
||||
// account. The row is written just before the live identities row is removed, preserving
|
||||
// the legal dossier (which email/vk/tg was linked, and when) even as the identity frees
|
||||
// for reuse. See docs/ARCHITECTURE.md §9.1.
|
||||
const (
|
||||
retainUnlink = "unlink"
|
||||
retainChange = "change"
|
||||
retainDelete = "delete"
|
||||
)
|
||||
|
||||
// retainIdentityTx appends a retention-journal row for one identity being detached, inside
|
||||
// tx. linkedAt is the identity's original creation time; detached_at defaults to now(). It
|
||||
// must run in the same transaction as the identity removal, so the dossier and the live
|
||||
// state can never diverge.
|
||||
func retainIdentityTx(ctx context.Context, tx *sql.Tx, accountID uuid.UUID, kind, externalID string, confirmed bool, linkedAt time.Time, reason string) error {
|
||||
id, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return fmt.Errorf("account: new retained id: %w", err)
|
||||
}
|
||||
ins := table.RetainedIdentities.INSERT(
|
||||
table.RetainedIdentities.RetainedID, table.RetainedIdentities.AccountID,
|
||||
table.RetainedIdentities.Kind, table.RetainedIdentities.ExternalID,
|
||||
table.RetainedIdentities.Confirmed, table.RetainedIdentities.LinkedAt,
|
||||
table.RetainedIdentities.Reason,
|
||||
).VALUES(id, accountID, kind, externalID, confirmed, linkedAt, reason)
|
||||
if _, err := ins.ExecContext(ctx, tx); err != nil {
|
||||
return fmt.Errorf("account: retain identity (%s, %s): %w", kind, externalID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user