94 lines
3.0 KiB
Go
94 lines
3.0 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// SoftDelete marks the account as soft-deleted with an audit trail of
|
|
// who initiated the operation, then drives the documented in-process
|
|
// cascade across `auth`, `lobby`, `notification`, and `geo`.
|
|
//
|
|
// The `accounts` row is the canonical state; cascade calls run after
|
|
// the database commit and are best-effort. Cascade failures are joined
|
|
// into the returned error and logged but never roll back the
|
|
// soft-delete: the producer signal is "this user is gone", and
|
|
// downstream cleanup is idempotent so a future retry can finish the
|
|
// job.
|
|
//
|
|
// Repeated calls on an already-soft-deleted account are no-ops: the
|
|
// store reports `false` for "row changed" and the cascade is skipped.
|
|
func (s *Service) SoftDelete(ctx context.Context, userID uuid.UUID, actor ActorRef) error {
|
|
if userID == uuid.Nil {
|
|
return ErrAccountNotFound
|
|
}
|
|
if err := actor.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
now := s.deps.Now().UTC()
|
|
changed, err := s.deps.Store.SoftDeleteAccount(ctx, userID, actor, now)
|
|
if err != nil {
|
|
return fmt.Errorf("user soft delete: %w", err)
|
|
}
|
|
if !changed {
|
|
s.deps.Logger.Info("user soft delete skipped (already deleted)",
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
return nil
|
|
}
|
|
s.deps.Logger.Info("user soft deleted",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("actor_type", actor.Type),
|
|
)
|
|
return s.runSoftDeleteCascade(ctx, userID, actor)
|
|
}
|
|
|
|
// runSoftDeleteCascade fans the soft-delete signal out to dependent
|
|
// modules in the documented order: auth → lobby → notification → geo.
|
|
// Each call's error is joined; the loop continues even after a
|
|
// failure so the remaining modules still get notified.
|
|
func (s *Service) runSoftDeleteCascade(ctx context.Context, userID uuid.UUID, actor ActorRef) error {
|
|
var joined error
|
|
if s.deps.SessionRevoker != nil {
|
|
kind := SessionRevokeActorSoftDeleteAdmin
|
|
if actor.Type == "user" {
|
|
kind = SessionRevokeActorSoftDeleteUser
|
|
}
|
|
revokeActor := SessionRevokeActor{
|
|
Kind: kind,
|
|
ID: actor.ID,
|
|
Reason: "soft delete",
|
|
}
|
|
if err := s.deps.SessionRevoker.RevokeAllForUser(ctx, userID, revokeActor); err != nil {
|
|
joined = errors.Join(joined, fmt.Errorf("session revoke: %w", err))
|
|
}
|
|
}
|
|
if s.deps.Lobby != nil {
|
|
if err := s.deps.Lobby.OnUserDeleted(ctx, userID); err != nil {
|
|
joined = errors.Join(joined, fmt.Errorf("lobby on-user-deleted: %w", err))
|
|
}
|
|
}
|
|
if s.deps.Notification != nil {
|
|
if err := s.deps.Notification.OnUserDeleted(ctx, userID); err != nil {
|
|
joined = errors.Join(joined, fmt.Errorf("notification on-user-deleted: %w", err))
|
|
}
|
|
}
|
|
if s.deps.Geo != nil {
|
|
if err := s.deps.Geo.OnUserDeleted(ctx, userID); err != nil {
|
|
joined = errors.Join(joined, fmt.Errorf("geo on-user-deleted: %w", err))
|
|
}
|
|
}
|
|
if joined != nil {
|
|
s.deps.Logger.Warn("soft-delete cascade returned errors",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Error(joined),
|
|
)
|
|
}
|
|
return joined
|
|
}
|