forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
37
Tiku.Domain/Catalog/TaxonomyEntities.cs
Normal file
37
Tiku.Domain/Catalog/TaxonomyEntities.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Text.Json;
|
||||
using Tiku.Domain.Common;
|
||||
|
||||
namespace Tiku.Domain.Catalog;
|
||||
|
||||
public sealed class TaxonomyNode : AuditableTenantEntity
|
||||
{
|
||||
public Guid? ParentOwnerTenantId { get; set; }
|
||||
public Guid? ParentId { get; set; }
|
||||
public TaxonomyNodeType NodeType { get; set; } = TaxonomyNodeType.KnowledgePoint;
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Path { get; set; }
|
||||
public int Depth { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class QuestionTaxonomyAssignment : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid QuestionId { get; set; }
|
||||
public Guid TaxonomyOwnerTenantId { get; set; }
|
||||
public Guid TaxonomyNodeId { get; set; }
|
||||
public bool IsPrimary { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public enum TaxonomyNodeType
|
||||
{
|
||||
Subject,
|
||||
Chapter,
|
||||
KnowledgePoint,
|
||||
Paper,
|
||||
Custom
|
||||
}
|
||||
@@ -89,7 +89,7 @@ public sealed class Payment : AuditableTenantEntity
|
||||
public JsonElement RawPayload { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class PaymentEvent : Entity
|
||||
public sealed class PaymentEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid? PaymentId { get; set; }
|
||||
@@ -103,7 +103,7 @@ public sealed class PaymentEvent : Entity
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class Entitlement : Entity
|
||||
public sealed class Entitlement : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
@@ -213,7 +213,7 @@ public sealed class TenantSubscription : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class TenantUsageRecord : Entity
|
||||
public sealed class TenantUsageRecord : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public string MetricKey { get; set; } = string.Empty;
|
||||
@@ -248,7 +248,7 @@ public sealed class CommerceRefundRequest : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class CommerceRefundEvent : Entity
|
||||
public sealed class CommerceRefundEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid RefundRequestId { get; set; }
|
||||
@@ -285,7 +285,7 @@ public sealed class CommerceReconciliationBatch : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class CommerceReconciliationItem : Entity
|
||||
public sealed class CommerceReconciliationItem : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid BatchId { get; set; }
|
||||
@@ -344,7 +344,7 @@ public sealed class CommerceReconciliationIssue : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class CommerceReconciliationIssueEvent : Entity
|
||||
public sealed class CommerceReconciliationIssueEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid IssueId { get; set; }
|
||||
|
||||
@@ -13,13 +13,18 @@ public interface IHasTimestamps
|
||||
DateTimeOffset UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
public interface ITenantOwned
|
||||
{
|
||||
Guid TenantId { get; set; }
|
||||
}
|
||||
|
||||
public abstract class AuditableEntity : Entity, IHasTimestamps
|
||||
{
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public abstract class TenantEntity : Entity
|
||||
public abstract class TenantEntity : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@ public enum ContentStatus
|
||||
public sealed class QuestionCollectionItem : AuditableTenantEntity
|
||||
{
|
||||
public Guid CollectionId { get; set; }
|
||||
public Guid QuestionReferenceId { get; set; }
|
||||
public Guid QuestionOwnerTenantId { get; set; }
|
||||
public Guid QuestionId { get; set; }
|
||||
public string? SectionKey { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
@@ -3,7 +3,7 @@ using Tiku.Domain.Common;
|
||||
|
||||
namespace Tiku.Domain.Content;
|
||||
|
||||
public sealed class ContentAssetAccessEvent : Entity
|
||||
public sealed class ContentAssetAccessEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid? AssetId { get; set; }
|
||||
@@ -24,7 +24,7 @@ public sealed class ContentAssetAccessEvent : Entity
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class ContentAssetSecurityScanEvent : Entity
|
||||
public sealed class ContentAssetSecurityScanEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid? AssetId { get; set; }
|
||||
@@ -36,38 +36,25 @@ public sealed class ContentAssetSecurityScanEvent : Entity
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class QuestionBankGrant : AuditableEntity
|
||||
public sealed class TenantQuestionBankPreference : AuditableTenantEntity
|
||||
{
|
||||
public Guid SourceQuestionBankId { get; set; }
|
||||
public Guid QuestionBankOwnerTenantId { get; set; }
|
||||
public Guid QuestionBankId { get; set; }
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public string? Alias { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public string? NavigationLocation { get; set; }
|
||||
public Guid? CreatedBy { get; set; }
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
public QuestionBankGrantScope GrantScope { get; set; } = QuestionBankGrantScope.Plans;
|
||||
public string[] AllowedPlanCodes { get; set; } = [];
|
||||
public Guid[] AllowedTenantIds { get; set; } = [];
|
||||
public Guid[] AllowedRegionIds { get; set; } = [];
|
||||
public Guid[] AllowedSubjectIds { get; set; } = [];
|
||||
public QuestionBankGrantStatus Status { get; set; } = QuestionBankGrantStatus.Active;
|
||||
public DateTimeOffset? StartsAt { get; set; }
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class TenantQuestionBankAdoption : AuditableTenantEntity
|
||||
public sealed class TenantQuestionReference : AuditableTenantEntity
|
||||
{
|
||||
public Guid SourceQuestionBankId { get; set; }
|
||||
public Guid? GrantId { get; set; }
|
||||
public Guid? TargetQuestionBankId { get; set; }
|
||||
public Guid? TargetEntryId { get; set; }
|
||||
public Guid? TargetCollectionId { get; set; }
|
||||
public Guid QuestionOwnerTenantId { get; set; }
|
||||
public Guid QuestionId { get; set; }
|
||||
public QuestionSource Source { get; set; } = QuestionSource.Tenant;
|
||||
public Guid? CreatedBy { get; set; }
|
||||
public Guid? UpdatedBy { get; set; }
|
||||
public QuestionBankAdoptionMode AdoptionMode { get; set; } = QuestionBankAdoptionMode.CopiedSnapshot;
|
||||
public QuestionBankAdoptionStatus Status { get; set; } = QuestionBankAdoptionStatus.Active;
|
||||
public QuestionBankAdoptionSyncStatus SyncStatus { get; set; } = QuestionBankAdoptionSyncStatus.Synced;
|
||||
public JsonElement SourceSnapshot { get; set; } = JsonDefaults.Object();
|
||||
public int CopiedQuestionCount { get; set; }
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
public DateTimeOffset? LastSyncedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AiRecommendationReport : AuditableTenantEntity
|
||||
@@ -90,9 +77,5 @@ public enum AssetAccessType { Download, Preview, AdminDownload, AdminPreview, Up
|
||||
public enum AssetAccessDisposition { Attachment, Inline }
|
||||
public enum AssetAccessResult { Granted, Denied }
|
||||
public enum AssetSecurityRiskLevel { None, Low, Medium, High, Critical }
|
||||
public enum QuestionBankGrantScope { AllActiveTenants, Plans, Tenants, Mixed }
|
||||
public enum QuestionBankGrantStatus { Active, Disabled, Expired }
|
||||
public enum QuestionBankAdoptionMode { CopiedSnapshot, Reference }
|
||||
public enum QuestionBankAdoptionStatus { Active, SyncPending, Suspended, Archived }
|
||||
public enum QuestionBankAdoptionSyncStatus { Pending, Synced, Failed }
|
||||
public enum QuestionSource { Platform, Tenant }
|
||||
public enum AiRecommendationReportStatus { Draft, Generated, Failed }
|
||||
|
||||
@@ -188,7 +188,7 @@ public sealed class CommissionSettlementProof : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class CommissionSettlementExportEvent : Entity
|
||||
public sealed class CommissionSettlementExportEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid SettlementId { get; set; }
|
||||
|
||||
@@ -3,7 +3,7 @@ using Tiku.Domain.Common;
|
||||
|
||||
namespace Tiku.Domain.Import;
|
||||
|
||||
public sealed class PocketBaseImportRun : Entity
|
||||
public sealed class PocketBaseImportRun : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public string SourceName { get; set; } = string.Empty;
|
||||
@@ -14,7 +14,7 @@ public sealed class PocketBaseImportRun : Entity
|
||||
public DateTimeOffset? FinishedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PocketBaseRawRecord
|
||||
public sealed class PocketBaseRawRecord : ITenantOwned
|
||||
{
|
||||
public Guid RunId { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
@@ -26,7 +26,7 @@ public sealed class PocketBaseRawRecord
|
||||
public DateTimeOffset ImportedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PocketBaseImportIssue : Entity
|
||||
public sealed class PocketBaseImportIssue : Entity, ITenantOwned
|
||||
{
|
||||
public Guid? RunId { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
@@ -15,7 +15,6 @@ public sealed class PracticeSession : TenantEntity
|
||||
public Guid? TargetId { get; set; }
|
||||
public DateTimeOffset StartedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset? FinishedAt { get; set; }
|
||||
public JsonElement QuestionIds { get; set; } = JsonDefaults.Array();
|
||||
public int QuestionCount { get; set; }
|
||||
public int? DurationMinutes { get; set; }
|
||||
public decimal? TotalScore { get; set; }
|
||||
@@ -27,6 +26,17 @@ public sealed class PracticeSession : TenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class PracticeSessionQuestion : TenantEntity
|
||||
{
|
||||
public Guid PracticeSessionId { get; set; }
|
||||
public Guid QuestionReferenceId { get; set; }
|
||||
public Guid QuestionOwnerTenantId { get; set; }
|
||||
public Guid QuestionId { get; set; }
|
||||
public Guid QuestionVersionId { get; set; }
|
||||
public int Position { get; set; }
|
||||
public decimal? Score { get; set; }
|
||||
}
|
||||
|
||||
public enum PracticeAccessMode
|
||||
{
|
||||
Free,
|
||||
@@ -37,9 +47,8 @@ public enum PracticeAccessMode
|
||||
public sealed class AnswerRecord : TenantEntity
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public Guid? QuestionId { get; set; }
|
||||
public Guid? QuestionVersionId { get; set; }
|
||||
public Guid? PracticeSessionId { get; set; }
|
||||
public Guid PracticeSessionId { get; set; }
|
||||
public Guid SessionQuestionId { get; set; }
|
||||
public string? LegacyId { get; set; }
|
||||
public string? LegacyQuestionId { get; set; }
|
||||
public string? LegacyCategoryId { get; set; }
|
||||
@@ -50,19 +59,23 @@ public sealed class AnswerRecord : TenantEntity
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class FavoriteQuestion
|
||||
public sealed class FavoriteQuestion : ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public Guid QuestionReferenceId { get; set; }
|
||||
public Guid QuestionOwnerTenantId { get; set; }
|
||||
public Guid QuestionId { get; set; }
|
||||
public string Source { get; set; } = "imported";
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class WrongQuestion
|
||||
public sealed class WrongQuestion : ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public Guid QuestionReferenceId { get; set; }
|
||||
public Guid QuestionOwnerTenantId { get; set; }
|
||||
public Guid QuestionId { get; set; }
|
||||
public int WrongCount { get; set; } = 1;
|
||||
public DateTimeOffset LastWrongAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed class Report : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class ReportStatusEvent : Entity
|
||||
public sealed class ReportStatusEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid ReportId { get; set; }
|
||||
@@ -48,7 +48,7 @@ public sealed class ReportStatusEvent : Entity
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class UserScoreEvent : Entity
|
||||
public sealed class UserScoreEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
@@ -73,7 +73,7 @@ public sealed class PracticeDailyUsage : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class PracticeAccessEvent : Entity
|
||||
public sealed class PracticeAccessEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class PlatformSaasPlan : AuditableEntity
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TenantBillingProfile : IHasTimestamps
|
||||
public sealed class TenantBillingProfile : IHasTimestamps, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public string? BillingName { get; set; }
|
||||
@@ -58,7 +58,7 @@ public sealed class TenantInvoice : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class TenantInvoiceItem : Entity
|
||||
public sealed class TenantInvoiceItem : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid InvoiceId { get; set; }
|
||||
|
||||
@@ -7,17 +7,10 @@ public sealed class QuestionBank : AuditableTenantEntity
|
||||
{
|
||||
public Guid? RegionId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public QuestionBankScope SourceScope { get; set; } = QuestionBankScope.Tenant;
|
||||
public QuestionBankStatus Status { get; set; } = QuestionBankStatus.Active;
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public enum QuestionBankScope
|
||||
{
|
||||
Platform,
|
||||
Tenant
|
||||
}
|
||||
|
||||
public enum QuestionBankStatus
|
||||
{
|
||||
Active,
|
||||
@@ -55,7 +48,7 @@ public enum QuestionStatus
|
||||
Archived
|
||||
}
|
||||
|
||||
public sealed class QuestionVersion : Entity
|
||||
public sealed class QuestionVersion : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid QuestionId { get; set; }
|
||||
|
||||
@@ -11,6 +11,10 @@ public sealed class TenantDomain : AuditableTenantEntity
|
||||
public bool IsPrimary { get; set; }
|
||||
public string? VerificationToken { get; set; }
|
||||
public DateTimeOffset? VerifiedAt { get; set; }
|
||||
public DateTimeOffset? LastCheckedAt { get; set; }
|
||||
public DateTimeOffset? DnsVerifiedAt { get; set; }
|
||||
public DateTimeOffset? TlsReadyAt { get; set; }
|
||||
public string? LastFailureReason { get; set; }
|
||||
}
|
||||
|
||||
public enum TenantDomainType
|
||||
@@ -28,7 +32,7 @@ public enum TenantDomainStatus
|
||||
Disabled
|
||||
}
|
||||
|
||||
public sealed class TenantBranding : IHasTimestamps
|
||||
public sealed class TenantBranding : IHasTimestamps, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public string BrandName { get; set; } = string.Empty;
|
||||
@@ -45,7 +49,7 @@ public sealed class TenantBranding : IHasTimestamps
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class TenantSettings : IHasTimestamps
|
||||
public sealed class TenantSettings : IHasTimestamps, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public JsonElement FeatureFlags { get; set; } = JsonDefaults.Object();
|
||||
@@ -54,3 +58,20 @@ public sealed class TenantSettings : IHasTimestamps
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class TenantFrontendConfig : AuditableTenantEntity
|
||||
{
|
||||
public int SchemaVersion { get; set; } = 1;
|
||||
public int ConfigVersion { get; set; } = 1;
|
||||
public JsonElement PublishedBranding { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement PublishedTheme { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement PublishedFeatures { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement PublishedNavigation { get; set; } = JsonDefaults.Array();
|
||||
public JsonElement PublishedHomeModules { get; set; } = JsonDefaults.Array();
|
||||
public JsonElement DraftBranding { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement DraftTheme { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement DraftFeatures { get; set; } = JsonDefaults.Object();
|
||||
public JsonElement DraftNavigation { get; set; } = JsonDefaults.Array();
|
||||
public JsonElement DraftHomeModules { get; set; } = JsonDefaults.Array();
|
||||
public DateTimeOffset? PublishedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class TenantSecret : AuditableTenantEntity
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SmsVerificationCode : Entity
|
||||
public sealed class SmsVerificationCode : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
@@ -43,7 +43,7 @@ public sealed class SmsVerificationCode : Entity
|
||||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class AuthLoginEvent : Entity
|
||||
public sealed class AuthLoginEvent : Entity, ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
@@ -69,7 +69,7 @@ public sealed class AuthSession : AuditableTenantEntity
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
}
|
||||
|
||||
public sealed class SmsSendRateLimit
|
||||
public sealed class SmsSendRateLimit : ITenantOwned
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public SmsRateLimitDimension Dimension { get; set; } = SmsRateLimitDimension.Phone;
|
||||
|
||||
Reference in New Issue
Block a user