ability to specify if a theme is enabled for a site
This commit is contained in:
@ -256,7 +256,7 @@ namespace Oqtane.Controllers
|
||||
}
|
||||
|
||||
// remove module definition
|
||||
_moduleDefinitions.DeleteModuleDefinition(id, siteid);
|
||||
_moduleDefinitions.DeleteModuleDefinition(id);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.ModuleDefinition, moduledefinition.ModuleDefinitionId, SyncEventActions.Delete);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Module Definition {ModuleDefinitionName} Deleted", moduledefinition.Name);
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ using Oqtane.Infrastructure;
|
||||
using Oqtane.Repository;
|
||||
using System.Text.Json;
|
||||
using System.Net;
|
||||
using System.Reflection.Metadata;
|
||||
using System;
|
||||
|
||||
// ReSharper disable StringIndexOfIsCultureSpecific.1
|
||||
|
||||
@ -23,14 +25,20 @@ namespace Oqtane.Controllers
|
||||
private readonly IThemeRepository _themes;
|
||||
private readonly IInstallationManager _installationManager;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly ITenantManager _tenantManager;
|
||||
private readonly ISyncManager _syncManager;
|
||||
private readonly ILogManager _logger;
|
||||
private readonly Alias _alias;
|
||||
|
||||
public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ILogManager logger)
|
||||
public ThemeController(IThemeRepository themes, IInstallationManager installationManager, IWebHostEnvironment environment, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger)
|
||||
{
|
||||
_themes = themes;
|
||||
_installationManager = installationManager;
|
||||
_environment = environment;
|
||||
_tenantManager = tenantManager;
|
||||
_syncManager = syncManager;
|
||||
_logger = logger;
|
||||
_alias = tenantManager.GetAlias();
|
||||
}
|
||||
|
||||
// GET: api/<controller>
|
||||
@ -41,6 +49,41 @@ namespace Oqtane.Controllers
|
||||
return _themes.GetThemes();
|
||||
}
|
||||
|
||||
// GET api/<controller>/5?siteid=x
|
||||
[HttpGet("{id}")]
|
||||
public Theme Get(int id, string siteid)
|
||||
{
|
||||
int SiteId;
|
||||
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
|
||||
{
|
||||
return _themes.GetTheme(id, SiteId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Get Attempt {ThemeId} {SiteId}", id, siteid);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// PUT api/<controller>/5
|
||||
[HttpPut("{id}")]
|
||||
[Authorize(Roles = RoleNames.Admin)]
|
||||
public void Put(int id, [FromBody] Theme theme)
|
||||
{
|
||||
if (ModelState.IsValid && theme.SiteId == _alias.SiteId && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null)
|
||||
{
|
||||
_themes.UpdateTheme(theme);
|
||||
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Theme Updated {Theme}", theme);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Theme Put Attempt {Theme}", theme);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE api/<controller>/xxx
|
||||
[HttpDelete("{themename}")]
|
||||
[Authorize(Roles = RoleNames.Host)]
|
||||
@ -74,7 +117,7 @@ namespace Oqtane.Controllers
|
||||
}
|
||||
|
||||
// remove theme
|
||||
_themes.DeleteTheme(theme.ThemeName);
|
||||
//_themes.DeleteTheme(theme.ThemeName);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Theme Removed For {ThemeName}", theme.ThemeName);
|
||||
}
|
||||
else
|
||||
|
@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations.Operations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
|
||||
using Oqtane.Databases.Interfaces;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
|
||||
namespace Oqtane.Migrations.EntityBuilders
|
||||
{
|
||||
public class ThemeEntityBuilder : AuditableBaseEntityBuilder<ThemeEntityBuilder>
|
||||
{
|
||||
private const string _entityTableName = "Theme";
|
||||
private readonly PrimaryKey<ThemeEntityBuilder> _primaryKey = new("PK_Theme", x => x.ThemeId);
|
||||
|
||||
public ThemeEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
|
||||
{
|
||||
EntityTableName = _entityTableName;
|
||||
PrimaryKey = _primaryKey;
|
||||
}
|
||||
|
||||
protected override ThemeEntityBuilder BuildTable(ColumnsBuilder table)
|
||||
{
|
||||
ThemeId = AddAutoIncrementColumn(table, "ThemeId");
|
||||
ThemeName = AddStringColumn(table, "ThemeName", 200);
|
||||
|
||||
AddAuditableColumns(table);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public OperationBuilder<AddColumnOperation> ThemeId { get; private set; }
|
||||
|
||||
public OperationBuilder<AddColumnOperation> ThemeName { get; private set; }
|
||||
}
|
||||
}
|
28
Oqtane.Server/Migrations/Master/04000001_AddThemeTable.cs
Normal file
28
Oqtane.Server/Migrations/Master/04000001_AddThemeTable.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Oqtane.Databases.Interfaces;
|
||||
using Oqtane.Migrations.EntityBuilders;
|
||||
using Oqtane.Repository;
|
||||
|
||||
namespace Oqtane.Migrations.Master
|
||||
{
|
||||
[DbContext(typeof(MasterDBContext))]
|
||||
[Migration("Master.04.00.00.01")]
|
||||
public class AddThemeTable : MultiDatabaseMigration
|
||||
{
|
||||
public AddThemeTable(IDatabase database) : base(database)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var themeEntityBuilder = new ThemeEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
themeEntityBuilder.Create();
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// not implemented
|
||||
}
|
||||
}
|
||||
}
|
@ -68,6 +68,7 @@ namespace Oqtane.Repository
|
||||
public virtual DbSet<Job> Job { get; set; }
|
||||
public virtual DbSet<JobLog> JobLog { get; set; }
|
||||
public virtual DbSet<Setting> Setting { get; set; }
|
||||
public virtual DbSet<Theme> Theme { get; set; }
|
||||
|
||||
public override int SaveChanges()
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ namespace Oqtane.Repository
|
||||
IEnumerable<ModuleDefinition> GetModuleDefinitions(int siteId);
|
||||
ModuleDefinition GetModuleDefinition(int moduleDefinitionId, int siteId);
|
||||
void UpdateModuleDefinition(ModuleDefinition moduleDefinition);
|
||||
void DeleteModuleDefinition(int moduleDefinitionId, int siteId);
|
||||
void DeleteModuleDefinition(int moduleDefinitionId);
|
||||
ModuleDefinition FilterModuleDefinition(ModuleDefinition moduleDefinition);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ namespace Oqtane.Repository
|
||||
public interface IThemeRepository
|
||||
{
|
||||
IEnumerable<Theme> GetThemes();
|
||||
Theme GetTheme(int themeId, int siteId);
|
||||
void UpdateTheme(Theme theme);
|
||||
void DeleteTheme(int themeId);
|
||||
List<Theme> FilterThemes(List<Theme> themes);
|
||||
void DeleteTheme(string ThemeName);
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ namespace Oqtane.Repository
|
||||
_cache.Remove($"moduledefinitions:{_tenants.GetAlias().SiteKey}");
|
||||
}
|
||||
|
||||
public void DeleteModuleDefinition(int moduleDefinitionId,int siteId)
|
||||
public void DeleteModuleDefinition(int moduleDefinitionId)
|
||||
{
|
||||
ModuleDefinition moduleDefinition = _db.ModuleDefinition.Find(moduleDefinitionId);
|
||||
_settings.DeleteSettings(EntityNames.ModuleDefinition, moduleDefinitionId);
|
||||
@ -126,48 +126,48 @@ namespace Oqtane.Repository
|
||||
private List<ModuleDefinition> ProcessModuleDefinitions(int siteId)
|
||||
{
|
||||
// get module assemblies
|
||||
List<ModuleDefinition> moduleDefinitions = LoadModuleDefinitionsFromAssemblies();
|
||||
List<ModuleDefinition> ModuleDefinitions = LoadModuleDefinitionsFromAssemblies();
|
||||
|
||||
// get module definitions in database
|
||||
List<ModuleDefinition> moduledefs = _db.ModuleDefinition.ToList();
|
||||
List<ModuleDefinition> moduledefinitions = _db.ModuleDefinition.ToList();
|
||||
|
||||
// sync module assemblies with database
|
||||
foreach (ModuleDefinition moduledefinition in moduleDefinitions)
|
||||
foreach (ModuleDefinition ModuleDefinition in ModuleDefinitions)
|
||||
{
|
||||
ModuleDefinition moduledef = moduledefs.Where(item => item.ModuleDefinitionName == moduledefinition.ModuleDefinitionName).FirstOrDefault();
|
||||
if (moduledef == null)
|
||||
ModuleDefinition moduledefinition = moduledefinitions.Where(item => item.ModuleDefinitionName == ModuleDefinition.ModuleDefinitionName).FirstOrDefault();
|
||||
if (moduledefinition == null)
|
||||
{
|
||||
// new module definition
|
||||
moduledef = new ModuleDefinition { ModuleDefinitionName = moduledefinition.ModuleDefinitionName };
|
||||
_db.ModuleDefinition.Add(moduledef);
|
||||
moduledefinition = new ModuleDefinition { ModuleDefinitionName = ModuleDefinition.ModuleDefinitionName };
|
||||
_db.ModuleDefinition.Add(moduledefinition);
|
||||
_db.SaveChanges();
|
||||
moduledefinition.Version = "";
|
||||
ModuleDefinition.Version = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
// override user customizable property values
|
||||
moduledefinition.Name = (!string.IsNullOrEmpty(moduledef.Name)) ? moduledef.Name : moduledefinition.Name;
|
||||
moduledefinition.Description = (!string.IsNullOrEmpty(moduledef.Description)) ? moduledef.Description : moduledefinition.Description;
|
||||
moduledefinition.Categories = (!string.IsNullOrEmpty(moduledef.Categories)) ? moduledef.Categories : moduledefinition.Categories;
|
||||
ModuleDefinition.Name = (!string.IsNullOrEmpty(moduledefinition.Name)) ? moduledefinition.Name : ModuleDefinition.Name;
|
||||
ModuleDefinition.Description = (!string.IsNullOrEmpty(moduledefinition.Description)) ? moduledefinition.Description : ModuleDefinition.Description;
|
||||
ModuleDefinition.Categories = (!string.IsNullOrEmpty(moduledefinition.Categories)) ? moduledefinition.Categories : ModuleDefinition.Categories;
|
||||
// manage releaseversions in cases where it was not provided or is lower than the module version
|
||||
if (string.IsNullOrEmpty(moduledefinition.ReleaseVersions) || Version.Parse(moduledefinition.Version).CompareTo(Version.Parse(moduledefinition.ReleaseVersions.Split(',').Last())) > 0)
|
||||
if (string.IsNullOrEmpty(ModuleDefinition.ReleaseVersions) || Version.Parse(ModuleDefinition.Version).CompareTo(Version.Parse(ModuleDefinition.ReleaseVersions.Split(',').Last())) > 0)
|
||||
{
|
||||
moduledefinition.ReleaseVersions = moduledefinition.Version;
|
||||
ModuleDefinition.ReleaseVersions = ModuleDefinition.Version;
|
||||
}
|
||||
moduledefinition.Version = moduledef.Version;
|
||||
ModuleDefinition.Version = moduledefinition.Version;
|
||||
// remove module definition from list as it is already synced
|
||||
moduledefs.Remove(moduledef);
|
||||
moduledefinitions.Remove(moduledefinition);
|
||||
}
|
||||
|
||||
moduledefinition.ModuleDefinitionId = moduledef.ModuleDefinitionId;
|
||||
moduledefinition.CreatedBy = moduledef.CreatedBy;
|
||||
moduledefinition.CreatedOn = moduledef.CreatedOn;
|
||||
moduledefinition.ModifiedBy = moduledef.ModifiedBy;
|
||||
moduledefinition.ModifiedOn = moduledef.ModifiedOn;
|
||||
ModuleDefinition.ModuleDefinitionId = moduledefinition.ModuleDefinitionId;
|
||||
ModuleDefinition.CreatedBy = moduledefinition.CreatedBy;
|
||||
ModuleDefinition.CreatedOn = moduledefinition.CreatedOn;
|
||||
ModuleDefinition.ModifiedBy = moduledefinition.ModifiedBy;
|
||||
ModuleDefinition.ModifiedOn = moduledefinition.ModifiedOn;
|
||||
}
|
||||
|
||||
// any remaining module definitions are orphans
|
||||
foreach (ModuleDefinition moduledefinition in moduledefs)
|
||||
foreach (ModuleDefinition moduledefinition in moduledefinitions)
|
||||
{
|
||||
_db.ModuleDefinition.Remove(moduledefinition); // delete
|
||||
_db.SaveChanges();
|
||||
@ -181,8 +181,8 @@ namespace Oqtane.Repository
|
||||
// get settings for site
|
||||
var settings = _settings.GetSettings(EntityNames.ModuleDefinition).ToList();
|
||||
|
||||
// populate module definition permissions
|
||||
foreach (ModuleDefinition moduledefinition in moduleDefinitions)
|
||||
// populate module definition site settings and permissions
|
||||
foreach (ModuleDefinition moduledefinition in ModuleDefinitions)
|
||||
{
|
||||
moduledefinition.SiteId = siteId;
|
||||
|
||||
@ -218,7 +218,7 @@ namespace Oqtane.Repository
|
||||
}
|
||||
|
||||
// clean up any orphaned permissions
|
||||
var ids = new HashSet<int>(moduleDefinitions.Select(item => item.ModuleDefinitionId));
|
||||
var ids = new HashSet<int>(ModuleDefinitions.Select(item => item.ModuleDefinitionId));
|
||||
foreach (var permission in permissions.Where(item => !ids.Contains(item.EntityId)))
|
||||
{
|
||||
try
|
||||
@ -232,7 +232,7 @@ namespace Oqtane.Repository
|
||||
}
|
||||
}
|
||||
|
||||
return moduleDefinitions;
|
||||
return ModuleDefinitions;
|
||||
}
|
||||
|
||||
private List<ModuleDefinition> LoadModuleDefinitionsFromAssemblies()
|
||||
|
@ -4,39 +4,168 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Shared;
|
||||
using Oqtane.Themes;
|
||||
using System.Reflection.Metadata;
|
||||
|
||||
namespace Oqtane.Repository
|
||||
{
|
||||
public class ThemeRepository : IThemeRepository
|
||||
{
|
||||
private MasterDBContext _db;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ITenantManager _tenants;
|
||||
private readonly ISettingRepository _settings;
|
||||
private readonly string settingprefix = "SiteEnabled:";
|
||||
|
||||
public ThemeRepository(IMemoryCache cache)
|
||||
public ThemeRepository(MasterDBContext context, IMemoryCache cache, ITenantManager tenants, ISettingRepository settings)
|
||||
{
|
||||
_db = context;
|
||||
_cache = cache;
|
||||
_tenants = tenants;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public IEnumerable<Theme> GetThemes()
|
||||
{
|
||||
return LoadThemes();
|
||||
// for consistency siteid should be passed in as parameter, but this would require breaking change
|
||||
return LoadThemes(_tenants.GetAlias().SiteId);
|
||||
}
|
||||
|
||||
private List<Theme> LoadThemes()
|
||||
public Theme GetTheme(int themeId, int siteId)
|
||||
{
|
||||
// get module definitions
|
||||
List<Theme> themes = _cache.GetOrCreate("themes", entry =>
|
||||
List<Theme> themes = LoadThemes(siteId);
|
||||
return themes.Find(item => item.ThemeId == themeId);
|
||||
}
|
||||
|
||||
public void UpdateTheme(Theme theme)
|
||||
{
|
||||
_db.Entry(theme).State = EntityState.Modified;
|
||||
_db.SaveChanges();
|
||||
|
||||
var settingname = $"{settingprefix}{_tenants.GetAlias().SiteKey}";
|
||||
var setting = _settings.GetSetting(EntityNames.Theme, theme.ThemeId, settingname);
|
||||
if (setting == null)
|
||||
{
|
||||
_settings.AddSetting(new Setting { EntityName = EntityNames.Theme, EntityId = theme.ThemeId, SettingName = settingname, SettingValue = theme.IsEnabled.ToString(), IsPrivate = true });
|
||||
}
|
||||
else
|
||||
{
|
||||
setting.SettingValue = theme.IsEnabled.ToString();
|
||||
_settings.UpdateSetting(setting);
|
||||
}
|
||||
|
||||
_cache.Remove($"themes:{_tenants.GetAlias().SiteKey}");
|
||||
}
|
||||
|
||||
public void DeleteTheme(int themeId)
|
||||
{
|
||||
Theme theme = _db.Theme.Find(themeId);
|
||||
_settings.DeleteSettings(EntityNames.Theme, themeId);
|
||||
_db.Theme.Remove(theme);
|
||||
_db.SaveChanges();
|
||||
_cache.Remove($"themes:{_tenants.GetAlias().SiteKey}");
|
||||
}
|
||||
|
||||
public List<Theme> FilterThemes(List<Theme> themes)
|
||||
{
|
||||
var Themes = new List<Theme>();
|
||||
|
||||
foreach (Theme theme in themes.Where(item => item.IsEnabled))
|
||||
{
|
||||
var Theme = new Theme();
|
||||
Theme.ThemeName = theme.ThemeName;
|
||||
Theme.Name = theme.Name;
|
||||
Theme.Resources = theme.Resources;
|
||||
Theme.Themes = theme.Themes;
|
||||
Theme.Containers = theme.Containers;
|
||||
Themes.Add(Theme);
|
||||
}
|
||||
|
||||
return Themes;
|
||||
}
|
||||
|
||||
private List<Theme> LoadThemes(int siteId)
|
||||
{
|
||||
// get themes
|
||||
List<Theme> themes = _cache.GetOrCreate($"themes:{_tenants.GetAlias().SiteKey}", entry =>
|
||||
{
|
||||
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
|
||||
return LoadThemesFromAssemblies();
|
||||
return ProcessThemes(siteId);
|
||||
});
|
||||
|
||||
return themes;
|
||||
}
|
||||
|
||||
private List<Theme> ProcessThemes(int siteId)
|
||||
{
|
||||
// get themes
|
||||
List<Theme> Themes = LoadThemesFromAssemblies();
|
||||
|
||||
// get themes in database
|
||||
List<Theme> themes = _db.Theme.ToList();
|
||||
|
||||
// sync theme assemblies with database
|
||||
foreach (Theme Theme in Themes)
|
||||
{
|
||||
Theme theme = themes.Where(item => item.ThemeName == Theme.ThemeName).FirstOrDefault();
|
||||
if (theme == null)
|
||||
{
|
||||
// new theme
|
||||
theme = new Theme { ThemeName = Theme.ThemeName };
|
||||
_db.Theme.Add(theme);
|
||||
_db.SaveChanges();
|
||||
}
|
||||
else
|
||||
{
|
||||
// remove theme from list as it is already synced
|
||||
themes.Remove(theme);
|
||||
}
|
||||
|
||||
Theme.ThemeId = theme.ThemeId;
|
||||
Theme.CreatedBy = theme.CreatedBy;
|
||||
Theme.CreatedOn = theme.CreatedOn;
|
||||
Theme.ModifiedBy = theme.ModifiedBy;
|
||||
Theme.ModifiedOn = theme.ModifiedOn;
|
||||
}
|
||||
|
||||
// any remaining themes are orphans
|
||||
foreach (Theme theme in themes)
|
||||
{
|
||||
_db.Theme.Remove(theme); // delete
|
||||
_db.SaveChanges();
|
||||
}
|
||||
|
||||
if (siteId != -1)
|
||||
{
|
||||
// get settings for site
|
||||
var settings = _settings.GetSettings(EntityNames.Theme).ToList();
|
||||
|
||||
// populate theme site settings
|
||||
foreach (Theme theme in Themes)
|
||||
{
|
||||
theme.SiteId = siteId;
|
||||
|
||||
var setting = settings.FirstOrDefault(item => item.EntityId == theme.ThemeId && item.SettingName == $"{settingprefix}{_tenants.GetAlias().SiteKey}");
|
||||
if (setting != null)
|
||||
{
|
||||
theme.IsEnabled = bool.Parse(setting.SettingValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
theme.IsEnabled = theme.IsAutoEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Themes;
|
||||
}
|
||||
|
||||
private List<Theme> LoadThemesFromAssemblies()
|
||||
{
|
||||
List<Theme> themes = new List<Theme>();
|
||||
@ -143,28 +272,5 @@ namespace Oqtane.Repository
|
||||
}
|
||||
return themes;
|
||||
}
|
||||
|
||||
public List<Theme> FilterThemes(List<Theme> themes)
|
||||
{
|
||||
var Themes = new List<Theme>();
|
||||
|
||||
foreach (Theme theme in themes)
|
||||
{
|
||||
var Theme = new Theme();
|
||||
Theme.ThemeName = theme.ThemeName;
|
||||
Theme.Name = theme.Name;
|
||||
Theme.Resources = theme.Resources;
|
||||
Theme.Themes = theme.Themes;
|
||||
Theme.Containers = theme.Containers;
|
||||
Themes.Add(Theme);
|
||||
}
|
||||
|
||||
return Themes;
|
||||
}
|
||||
|
||||
public void DeleteTheme(string ThemeName)
|
||||
{
|
||||
_cache.Remove("themes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user