package user import ( "context" "github.com/google/uuid" "go.uber.org/zap" ) // LobbyCascade collects the lobby-side hooks the user lifecycle invokes // after a successful soft-delete or permanent-block transition. The // real implementation lives in `backend/internal/lobby`. // Until then `NewNoopLobbyCascade` satisfies the contract. type LobbyCascade interface { OnUserDeleted(ctx context.Context, userID uuid.UUID) error OnUserBlocked(ctx context.Context, userID uuid.UUID) error } // NotificationCascade collects the notification-side hooks invoked at // soft-delete. The real implementation lives in // `backend/internal/notification`. type NotificationCascade interface { OnUserDeleted(ctx context.Context, userID uuid.UUID) error } // GeoCascade collects the geo-side hooks invoked at soft-delete. The // real implementation is `*geo.Service` once The implementation lands the // `OnUserDeleted` method. type GeoCascade interface { OnUserDeleted(ctx context.Context, userID uuid.UUID) error } // SessionRevoker revokes every active session bound to a user. The // canonical implementation wraps `*auth.Service.RevokeAllForUser`. The // adapter lives in `cmd/backend/main.go` so `auth` does not export an // extra method shape. // // The actor argument carries audit context: who initiated the revoke // and why. The auth side persists it into `session_revocations`; user // callers populate it with a fixed kind matching the trigger. type SessionRevoker interface { RevokeAllForUser(ctx context.Context, userID uuid.UUID, actor SessionRevokeActor) error } // SessionRevokeActor describes the principal behind a session revoke. // Kind is a closed vocabulary mirrored by `auth.ActorKind`; ID is the // stable identifier of the principal (a user UUID for self-driven // flows, an admin username for admin-driven flows). Reason is a // free-form note recorded in the audit row. type SessionRevokeActor struct { Kind string ID string Reason string } // Closed Kind vocabulary. Mirror constants live in // `auth.ActorKind*`; the values must stay in sync because the auth // adapter forwards them verbatim. const ( SessionRevokeActorSoftDeleteUser = "soft_delete_user" SessionRevokeActorSoftDeleteAdmin = "soft_delete_admin" SessionRevokeActorAdminSanction = "admin_sanction" ) // NewNoopLobbyCascade returns a LobbyCascade that logs every invocation // at info level and returns nil. The canonical lobby is wired in `cmd/backend/main.go`. // implementation; until then the no-op keeps the cascade orchestration // callable end-to-end. func NewNoopLobbyCascade(logger *zap.Logger) LobbyCascade { if logger == nil { logger = zap.NewNop() } return &noopLobbyCascade{logger: logger.Named("user.lobby.noop")} } type noopLobbyCascade struct { logger *zap.Logger } func (c *noopLobbyCascade) OnUserDeleted(_ context.Context, userID uuid.UUID) error { c.logger.Info("lobby on-user-deleted (noop cascade)", zap.String("user_id", userID.String())) return nil } func (c *noopLobbyCascade) OnUserBlocked(_ context.Context, userID uuid.UUID) error { c.logger.Info("lobby on-user-blocked (noop cascade)", zap.String("user_id", userID.String())) return nil } // NewNoopNotificationCascade returns a NotificationCascade that logs // every invocation at info level and returns nil. The canonical implementation replaces // it with the real notification implementation. func NewNoopNotificationCascade(logger *zap.Logger) NotificationCascade { if logger == nil { logger = zap.NewNop() } return &noopNotificationCascade{logger: logger.Named("user.notification.noop")} } type noopNotificationCascade struct { logger *zap.Logger } func (c *noopNotificationCascade) OnUserDeleted(_ context.Context, userID uuid.UUID) error { c.logger.Info("notification on-user-deleted (noop cascade)", zap.String("user_id", userID.String())) return nil }