mirror of
https://github.com/bitwarden/server
synced 2025-12-25 12:43:14 +00:00
[AC-1895] AC Team code ownership moves: Bitwarden Portal (#3528)
--------- Co-authored-by: Addison Beck <hello@addisonbeck.com>
This commit is contained in:
@@ -1,68 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class CreateProviderModel : IValidatableObject
|
||||
{
|
||||
public CreateProviderModel() { }
|
||||
|
||||
[Display(Name = "Provider Type")]
|
||||
public ProviderType Type { get; set; }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OwnerEmail { get; set; }
|
||||
|
||||
[Display(Name = "Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
|
||||
[Display(Name = "Primary Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
|
||||
public virtual Provider ToProvider()
|
||||
{
|
||||
return new Provider()
|
||||
{
|
||||
Type = Type,
|
||||
Name = Name,
|
||||
BusinessName = BusinessName,
|
||||
BillingEmail = BillingEmail?.ToLowerInvariant().Trim()
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||
{
|
||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
|
||||
yield return new ValidationResult($"The {nameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BusinessName))
|
||||
{
|
||||
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
|
||||
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BillingEmail))
|
||||
{
|
||||
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
|
||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class OrganizationEditModel : OrganizationViewModel
|
||||
{
|
||||
public OrganizationEditModel() { }
|
||||
|
||||
public OrganizationEditModel(Provider provider)
|
||||
{
|
||||
Provider = provider;
|
||||
BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty;
|
||||
PlanType = Core.Enums.PlanType.TeamsMonthly;
|
||||
Plan = Core.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName();
|
||||
LicenseKey = RandomLicenseKey;
|
||||
}
|
||||
|
||||
public OrganizationEditModel(Organization org, Provider provider, IEnumerable<OrganizationUserUserDetails> orgUsers,
|
||||
IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections, IEnumerable<Group> groups,
|
||||
IEnumerable<Policy> policies, BillingInfo billingInfo, IEnumerable<OrganizationConnection> connections,
|
||||
GlobalSettings globalSettings, int secrets, int projects, int serviceAccounts, int occupiedSmSeats)
|
||||
: base(org, provider, connections, orgUsers, ciphers, collections, groups, policies, secrets, projects,
|
||||
serviceAccounts, occupiedSmSeats)
|
||||
{
|
||||
BillingInfo = billingInfo;
|
||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
|
||||
Name = org.Name;
|
||||
BusinessName = org.BusinessName;
|
||||
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
|
||||
PlanType = org.PlanType;
|
||||
Plan = org.Plan;
|
||||
Seats = org.Seats;
|
||||
MaxAutoscaleSeats = org.MaxAutoscaleSeats;
|
||||
MaxCollections = org.MaxCollections;
|
||||
UsePolicies = org.UsePolicies;
|
||||
UseSso = org.UseSso;
|
||||
UseKeyConnector = org.UseKeyConnector;
|
||||
UseScim = org.UseScim;
|
||||
UseGroups = org.UseGroups;
|
||||
UseDirectory = org.UseDirectory;
|
||||
UseEvents = org.UseEvents;
|
||||
UseTotp = org.UseTotp;
|
||||
Use2fa = org.Use2fa;
|
||||
UseApi = org.UseApi;
|
||||
UseSecretsManager = org.UseSecretsManager;
|
||||
UseResetPassword = org.UseResetPassword;
|
||||
SelfHost = org.SelfHost;
|
||||
UsersGetPremium = org.UsersGetPremium;
|
||||
UseCustomPermissions = org.UseCustomPermissions;
|
||||
MaxStorageGb = org.MaxStorageGb;
|
||||
Gateway = org.Gateway;
|
||||
GatewayCustomerId = org.GatewayCustomerId;
|
||||
GatewaySubscriptionId = org.GatewaySubscriptionId;
|
||||
Enabled = org.Enabled;
|
||||
LicenseKey = org.LicenseKey;
|
||||
ExpirationDate = org.ExpirationDate;
|
||||
SmSeats = org.SmSeats;
|
||||
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
}
|
||||
|
||||
public BillingInfo BillingInfo { get; set; }
|
||||
public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);
|
||||
public string FourteenDayExpirationDate => DateTime.Now.AddDays(14).ToString("yyyy-MM-ddTHH:mm");
|
||||
public string BraintreeMerchantId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Organization Name")]
|
||||
public string Name { get; set; }
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
[Display(Name = "Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
[Required]
|
||||
[Display(Name = "Plan")]
|
||||
public PlanType? PlanType { get; set; }
|
||||
[Required]
|
||||
[Display(Name = "Plan Name")]
|
||||
public string Plan { get; set; }
|
||||
[Display(Name = "Seats")]
|
||||
public int? Seats { get; set; }
|
||||
[Display(Name = "Max. Autoscale Seats")]
|
||||
public int? MaxAutoscaleSeats { get; set; }
|
||||
[Display(Name = "Max. Collections")]
|
||||
public short? MaxCollections { get; set; }
|
||||
[Display(Name = "Policies")]
|
||||
public bool UsePolicies { get; set; }
|
||||
[Display(Name = "SSO")]
|
||||
public bool UseSso { get; set; }
|
||||
[Display(Name = "Key Connector with Customer Encryption")]
|
||||
public bool UseKeyConnector { get; set; }
|
||||
[Display(Name = "Groups")]
|
||||
public bool UseGroups { get; set; }
|
||||
[Display(Name = "Directory")]
|
||||
public bool UseDirectory { get; set; }
|
||||
[Display(Name = "Events")]
|
||||
public bool UseEvents { get; set; }
|
||||
[Display(Name = "TOTP")]
|
||||
public bool UseTotp { get; set; }
|
||||
[Display(Name = "2FA")]
|
||||
public bool Use2fa { get; set; }
|
||||
[Display(Name = "API")]
|
||||
public bool UseApi { get; set; }
|
||||
[Display(Name = "Reset Password")]
|
||||
public bool UseResetPassword { get; set; }
|
||||
[Display(Name = "SCIM")]
|
||||
public bool UseScim { get; set; }
|
||||
[Display(Name = "Secrets Manager")]
|
||||
public bool UseSecretsManager { get; set; }
|
||||
[Display(Name = "Self Host")]
|
||||
public bool SelfHost { get; set; }
|
||||
[Display(Name = "Users Get Premium")]
|
||||
public bool UsersGetPremium { get; set; }
|
||||
[Display(Name = "Custom Permissions")]
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
[Display(Name = "Max. Storage GB")]
|
||||
public short? MaxStorageGb { get; set; }
|
||||
[Display(Name = "Gateway")]
|
||||
public GatewayType? Gateway { get; set; }
|
||||
[Display(Name = "Gateway Customer Id")]
|
||||
public string GatewayCustomerId { get; set; }
|
||||
[Display(Name = "Gateway Subscription Id")]
|
||||
public string GatewaySubscriptionId { get; set; }
|
||||
[Display(Name = "Enabled")]
|
||||
public bool Enabled { get; set; }
|
||||
[Display(Name = "License Key")]
|
||||
public string LicenseKey { get; set; }
|
||||
[Display(Name = "Expiration Date")]
|
||||
public DateTime? ExpirationDate { get; set; }
|
||||
public bool SalesAssistedTrialStarted { get; set; }
|
||||
[Display(Name = "Seats")]
|
||||
public int? SmSeats { get; set; }
|
||||
[Display(Name = "Max Autoscale Seats")]
|
||||
public int? MaxAutoscaleSmSeats { get; set; }
|
||||
[Display(Name = "Service Accounts")]
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
[Display(Name = "Max Autoscale Service Accounts")]
|
||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||
|
||||
/**
|
||||
* Creates a Plan[] object for use in Javascript
|
||||
* This is mapped manually below to provide some type safety in case the plan objects change
|
||||
* Add mappings for individual properties as you need them
|
||||
*/
|
||||
public IEnumerable<Dictionary<string, object>> GetPlansHelper() =>
|
||||
StaticStore.Plans
|
||||
.Where(p => p.SupportsSecretsManager)
|
||||
.Select(p => new Dictionary<string, object>
|
||||
{
|
||||
{ "type", p.Type },
|
||||
{ "baseServiceAccount", p.SecretsManager.BaseServiceAccount }
|
||||
});
|
||||
|
||||
public Organization CreateOrganization(Provider provider, bool flexibleCollectionsSignupEnabled, bool flexibleCollectionsV1Enabled)
|
||||
{
|
||||
BillingEmail = provider.BillingEmail;
|
||||
|
||||
var newOrg = new Organization
|
||||
{
|
||||
// This feature flag indicates that new organizations should be automatically onboarded to
|
||||
// Flexible Collections enhancements
|
||||
FlexibleCollections = flexibleCollectionsSignupEnabled,
|
||||
// These collection management settings smooth the migration for existing organizations by disabling some FC behavior.
|
||||
// If the organization is onboarded to Flexible Collections on signup, we turn them OFF to enable all new behaviour.
|
||||
// If the organization is NOT onboarded now, they will have to be migrated later, so they default to ON to limit FC changes on migration.
|
||||
LimitCollectionCreationDeletion = !flexibleCollectionsSignupEnabled,
|
||||
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1Enabled
|
||||
};
|
||||
return ToOrganization(newOrg);
|
||||
}
|
||||
|
||||
public Organization ToOrganization(Organization existingOrganization)
|
||||
{
|
||||
existingOrganization.Name = Name;
|
||||
existingOrganization.BusinessName = BusinessName;
|
||||
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingOrganization.PlanType = PlanType.Value;
|
||||
existingOrganization.Plan = Plan;
|
||||
existingOrganization.Seats = Seats;
|
||||
existingOrganization.MaxCollections = MaxCollections;
|
||||
existingOrganization.UsePolicies = UsePolicies;
|
||||
existingOrganization.UseSso = UseSso;
|
||||
existingOrganization.UseKeyConnector = UseKeyConnector;
|
||||
existingOrganization.UseScim = UseScim;
|
||||
existingOrganization.UseGroups = UseGroups;
|
||||
existingOrganization.UseDirectory = UseDirectory;
|
||||
existingOrganization.UseEvents = UseEvents;
|
||||
existingOrganization.UseTotp = UseTotp;
|
||||
existingOrganization.Use2fa = Use2fa;
|
||||
existingOrganization.UseApi = UseApi;
|
||||
existingOrganization.UseSecretsManager = UseSecretsManager;
|
||||
existingOrganization.UseResetPassword = UseResetPassword;
|
||||
existingOrganization.SelfHost = SelfHost;
|
||||
existingOrganization.UsersGetPremium = UsersGetPremium;
|
||||
existingOrganization.UseCustomPermissions = UseCustomPermissions;
|
||||
existingOrganization.MaxStorageGb = MaxStorageGb;
|
||||
existingOrganization.Gateway = Gateway;
|
||||
existingOrganization.GatewayCustomerId = GatewayCustomerId;
|
||||
existingOrganization.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
existingOrganization.Enabled = Enabled;
|
||||
existingOrganization.LicenseKey = LicenseKey;
|
||||
existingOrganization.ExpirationDate = ExpirationDate;
|
||||
existingOrganization.MaxAutoscaleSeats = MaxAutoscaleSeats;
|
||||
existingOrganization.SmSeats = SmSeats;
|
||||
existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats;
|
||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||
return existingOrganization;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class OrganizationSelectableViewModel : Organization
|
||||
{
|
||||
public bool Selected { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class OrganizationUnassignedToProviderSearchViewModel : PagedModel<OrganizationSelectableViewModel>
|
||||
{
|
||||
[Display(Name = "Organization Name")]
|
||||
public string OrganizationName { get; set; }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OrganizationOwnerEmail { get; set; }
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class OrganizationViewModel
|
||||
{
|
||||
public OrganizationViewModel() { }
|
||||
|
||||
public OrganizationViewModel(Organization org, Provider provider, IEnumerable<OrganizationConnection> connections,
|
||||
IEnumerable<OrganizationUserUserDetails> orgUsers, IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
||||
IEnumerable<Group> groups, IEnumerable<Policy> policies, int secretsCount, int projectCount, int serviceAccountsCount,
|
||||
int occupiedSmSeatsCount)
|
||||
|
||||
{
|
||||
Organization = org;
|
||||
Provider = provider;
|
||||
Connections = connections ?? Enumerable.Empty<OrganizationConnection>();
|
||||
HasPublicPrivateKeys = org.PublicKey != null && org.PrivateKey != null;
|
||||
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
|
||||
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
|
||||
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
|
||||
OccupiedSeatCount = UserInvitedCount + UserAcceptedCount + UserConfirmedCount;
|
||||
CipherCount = ciphers.Count();
|
||||
CollectionCount = collections.Count();
|
||||
GroupCount = groups?.Count() ?? 0;
|
||||
PolicyCount = policies?.Count() ?? 0;
|
||||
var organizationUserStatus = org.Status == OrganizationStatusType.Pending
|
||||
? OrganizationUserStatusType.Invited
|
||||
: OrganizationUserStatusType.Confirmed;
|
||||
Owners = string.Join(", ",
|
||||
orgUsers
|
||||
.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus)
|
||||
.Select(u => u.Email));
|
||||
Admins = string.Join(", ",
|
||||
orgUsers
|
||||
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
|
||||
.Select(u => u.Email));
|
||||
SecretsCount = secretsCount;
|
||||
ProjectsCount = projectCount;
|
||||
ServiceAccountsCount = serviceAccountsCount;
|
||||
OccupiedSmSeatsCount = occupiedSmSeatsCount;
|
||||
}
|
||||
|
||||
public Organization Organization { get; set; }
|
||||
public Provider Provider { get; set; }
|
||||
public IEnumerable<OrganizationConnection> Connections { get; set; }
|
||||
public string Owners { get; set; }
|
||||
public string Admins { get; set; }
|
||||
public int UserInvitedCount { get; set; }
|
||||
public int UserConfirmedCount { get; set; }
|
||||
public int UserAcceptedCount { get; set; }
|
||||
public int OccupiedSeatCount { get; set; }
|
||||
public int CipherCount { get; set; }
|
||||
public int CollectionCount { get; set; }
|
||||
public int GroupCount { get; set; }
|
||||
public int PolicyCount { get; set; }
|
||||
public bool HasPublicPrivateKeys { get; set; }
|
||||
public int SecretsCount { get; set; }
|
||||
public int ProjectsCount { get; set; }
|
||||
public int ServiceAccountsCount { get; set; }
|
||||
public int OccupiedSmSeatsCount { get; set; }
|
||||
public bool UseSecretsManager => Organization.UseSecretsManager;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class OrganizationsModel : PagedModel<Organization>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public bool? Paid { get; set; }
|
||||
public string Action { get; set; }
|
||||
public bool SelfHosted { get; set; }
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class ProviderEditModel : ProviderViewModel
|
||||
{
|
||||
public ProviderEditModel() { }
|
||||
|
||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||
: base(provider, providerUsers, organizations)
|
||||
{
|
||||
Name = provider.Name;
|
||||
BusinessName = provider.BusinessName;
|
||||
BillingEmail = provider.BillingEmail;
|
||||
BillingPhone = provider.BillingPhone;
|
||||
}
|
||||
|
||||
[Display(Name = "Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
[Display(Name = "Billing Phone Number")]
|
||||
public string BillingPhone { get; set; }
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
public string Name { get; set; }
|
||||
[Display(Name = "Events")]
|
||||
|
||||
public Provider ToProvider(Provider existingProvider)
|
||||
{
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
|
||||
return existingProvider;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class ProviderViewModel
|
||||
{
|
||||
public ProviderViewModel() { }
|
||||
|
||||
public ProviderViewModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||
{
|
||||
Provider = provider;
|
||||
UserCount = providerUsers.Count();
|
||||
ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin);
|
||||
|
||||
ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);
|
||||
}
|
||||
|
||||
public int UserCount { get; set; }
|
||||
public Provider Provider { get; set; }
|
||||
public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; }
|
||||
public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class ProvidersModel : PagedModel<Provider>
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string UserEmail { get; set; }
|
||||
public bool? Paid { get; set; }
|
||||
public string Action { get; set; }
|
||||
public bool SelfHosted { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user