Add administrator editing
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web.Mvc;
|
using System.Web.Mvc;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.QueryableExtensions;
|
|
||||||
using InventoryTraker.Web.Attributes;
|
using InventoryTraker.Web.Attributes;
|
||||||
using InventoryTraker.Web.Core;
|
using InventoryTraker.Web.Core;
|
||||||
using InventoryTraker.Web.Identity;
|
using InventoryTraker.Web.Identity;
|
||||||
@@ -11,6 +10,7 @@ using Microsoft.AspNet.Identity;
|
|||||||
|
|
||||||
namespace InventoryTraker.Web.Controllers
|
namespace InventoryTraker.Web.Controllers
|
||||||
{
|
{
|
||||||
|
[Authorize(Roles = ApplicationRoleManager.AdminRoleName)]
|
||||||
public class UserController : ControllerBase
|
public class UserController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ApplicationUserManager _userManager;
|
private readonly ApplicationUserManager _userManager;
|
||||||
@@ -30,12 +30,17 @@ namespace InventoryTraker.Web.Controllers
|
|||||||
public JsonResult All()
|
public JsonResult All()
|
||||||
{
|
{
|
||||||
var users =
|
var users =
|
||||||
_userManager
|
from u in _userManager.Users.ToList()
|
||||||
.Users
|
let ad = _userManager.GetRoles(u.Id).Contains(ApplicationRoleManager.AdminRoleName)
|
||||||
.ProjectTo<UserViewModel>(_mapper.ConfigurationProvider)
|
orderby u.UserName
|
||||||
.OrderBy(u => u.UserName);
|
select new UserViewModel
|
||||||
|
{
|
||||||
|
UserName = u.UserName,
|
||||||
|
Email = u.Email,
|
||||||
|
Administrator = ad
|
||||||
|
};
|
||||||
|
|
||||||
return BetterJson(users);
|
return BetterJson(users.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[ActionLog]
|
[ActionLog]
|
||||||
@@ -57,7 +62,17 @@ namespace InventoryTraker.Web.Controllers
|
|||||||
if (!identityResult.Succeeded)
|
if (!identityResult.Succeeded)
|
||||||
return GetErrorListJson(identityResult.Errors.ToArray());
|
return GetErrorListJson(identityResult.Errors.ToArray());
|
||||||
|
|
||||||
return BetterJson(_mapper.Map<UserViewModel>(user));
|
user = _userManager.FindByEmail(form.Email);
|
||||||
|
if (form.Administrator)
|
||||||
|
{
|
||||||
|
var result = _userManager.AddToRole(user.Id, ApplicationRoleManager.AdminRoleName);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
return GetErrorListJson(result.Errors.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
var userViewModel = _mapper.Map<UserViewModel>(user);
|
||||||
|
userViewModel.Administrator = _userManager.IsInRole(user.Id, ApplicationRoleManager.AdminRoleName);
|
||||||
|
return BetterJson(userViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
[ActionLog]
|
[ActionLog]
|
||||||
@@ -79,12 +94,33 @@ namespace InventoryTraker.Web.Controllers
|
|||||||
return GetErrorListJson(resetResult.Errors.ToArray());
|
return GetErrorListJson(resetResult.Errors.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rolesForUser = _userManager.GetRoles(user.Id);
|
||||||
|
|
||||||
|
if (rolesForUser.Contains(ApplicationRoleManager.AdminRoleName) && !form.Administrator)
|
||||||
|
{
|
||||||
|
var currentUser = _userManager.FindById(User.Identity.GetUserId());
|
||||||
|
if (currentUser == user)
|
||||||
|
return GetErrorListJson("Cannot remove admin from yourself");
|
||||||
|
|
||||||
|
var result = _userManager.RemoveFromRole(user.Id, ApplicationRoleManager.AdminRoleName);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
return GetErrorListJson(result.Errors.ToArray());
|
||||||
|
}
|
||||||
|
else if (!rolesForUser.Contains(ApplicationRoleManager.AdminRoleName) && form.Administrator)
|
||||||
|
{
|
||||||
|
var result = _userManager.AddToRole(user.Id, ApplicationRoleManager.AdminRoleName);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
return GetErrorListJson(result.Errors.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
var identityResult = _userManager.Update(user);
|
var identityResult = _userManager.Update(user);
|
||||||
|
|
||||||
if (!identityResult.Succeeded)
|
if (!identityResult.Succeeded)
|
||||||
return GetErrorListJson(identityResult.Errors.ToArray());
|
return GetErrorListJson(identityResult.Errors.ToArray());
|
||||||
|
|
||||||
return BetterJson(_mapper.Map<UserViewModel>(user));
|
var userViewModel = _mapper.Map<UserViewModel>(user);
|
||||||
|
userViewModel.Administrator = _userManager.IsInRole(user.Id, ApplicationRoleManager.AdminRoleName);
|
||||||
|
return BetterJson(userViewModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Web.Mvc;
|
using System.Web.Mvc;
|
||||||
using System.Web.Optimization;
|
using System.Web.Optimization;
|
||||||
using System.Web.Routing;
|
using System.Web.Routing;
|
||||||
|
using InventoryTraker.Web.Migrations;
|
||||||
|
|
||||||
namespace InventoryTraker.Web
|
namespace InventoryTraker.Web
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,7 @@ namespace InventoryTraker.Web
|
|||||||
BundleConfig.RegisterBundles(BundleTable.Bundles);
|
BundleConfig.RegisterBundles(BundleTable.Bundles);
|
||||||
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
|
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
|
||||||
EFConfig.Initialize();
|
EFConfig.Initialize();
|
||||||
|
SeedData.AddAdminRole();
|
||||||
//SeedData.Init();
|
//SeedData.Init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,41 +75,69 @@ namespace InventoryTraker.Web.Helpers
|
|||||||
|
|
||||||
var expression = ExpressionForInternal(property);
|
var expression = ExpressionForInternal(property);
|
||||||
|
|
||||||
//Creates <div class="form-group has-feedback"
|
|
||||||
// form-group-validation="Name">
|
|
||||||
var formGroup = new HtmlTag("div")
|
|
||||||
.AddClasses("form-group", "has-feedback")
|
|
||||||
.Attr("form-group-validation", name);
|
|
||||||
|
|
||||||
var labelText = metadata.DisplayName ?? name.Humanize(LetterCasing.Title);
|
var labelText = metadata.DisplayName ?? name.Humanize(LetterCasing.Title);
|
||||||
|
|
||||||
//Creates <label class="control-label" for="Name">Name</label>
|
//Creates <label class="control-label" for="Name">Name</label>
|
||||||
var label = new HtmlTag("label")
|
var label = new HtmlTag("label")
|
||||||
.AddClass("control-label")
|
|
||||||
.Attr("for", name)
|
.Attr("for", name)
|
||||||
.Text(labelText);
|
.Text(labelText);
|
||||||
|
|
||||||
var tagName = metadata.DataTypeName == "MultilineText"
|
var tagName =
|
||||||
|
metadata.DataTypeName == "MultilineText"
|
||||||
? "textarea"
|
? "textarea"
|
||||||
: "input";
|
: "input";
|
||||||
|
|
||||||
var placeholder = metadata.Watermark ??
|
|
||||||
(labelText + "...");
|
|
||||||
//Creates <input ng-model="expression"
|
//Creates <input ng-model="expression"
|
||||||
// class="form-control" name="Name" type="text" >
|
// class="form-control" name="Name" type="text" >
|
||||||
var input = new HtmlTag(tagName)
|
var input = new HtmlTag(tagName)
|
||||||
.AddClass("form-control")
|
|
||||||
.Attr("ng-model", expression)
|
.Attr("ng-model", expression)
|
||||||
.Attr("name", name)
|
.Attr("name", name);
|
||||||
|
|
||||||
|
var formGroup = new HtmlTag("div");
|
||||||
|
|
||||||
|
if (metadata.ModelType != typeof(bool))
|
||||||
|
{
|
||||||
|
label.AddClass("control-label");
|
||||||
|
|
||||||
|
var placeholder = metadata.Watermark ??
|
||||||
|
labelText + "...";
|
||||||
|
|
||||||
|
input
|
||||||
|
.AddClass("form-control")
|
||||||
.Attr("type", "text")
|
.Attr("type", "text")
|
||||||
.Attr("placeholder", placeholder);
|
.Attr("placeholder", placeholder);
|
||||||
|
|
||||||
ApplyValidationToInput(input, metadata);
|
ApplyValidationToInput(input, metadata);
|
||||||
|
|
||||||
return formGroup
|
//Creates <div class="form-group has-feedback"
|
||||||
|
// form-group-validation="Name">
|
||||||
|
formGroup
|
||||||
|
.AddClass("form-group")
|
||||||
|
.AddClass("has-feedback")
|
||||||
|
.Attr("form-group-validation", name)
|
||||||
.Append(label)
|
.Append(label)
|
||||||
.Append(input);
|
.Append(input);
|
||||||
}
|
}
|
||||||
|
else if (metadata.ModelType == typeof(bool))
|
||||||
|
{
|
||||||
|
label.AddClass("form-check-label");
|
||||||
|
|
||||||
|
input
|
||||||
|
.AddClass("form-check-input")
|
||||||
|
.Attr("type", "checkbox");
|
||||||
|
|
||||||
|
label.Text("")
|
||||||
|
.Append(input)
|
||||||
|
.AppendHtml(" ")
|
||||||
|
.Append(new HtmlTag("text").NoTag().Text(labelText));
|
||||||
|
|
||||||
|
formGroup
|
||||||
|
.AddClass("form-check")
|
||||||
|
.Append(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formGroup;
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyValidationToInput(HtmlTag input, ModelMetadata metadata)
|
private void ApplyValidationToInput(HtmlTag input, ModelMetadata metadata)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,11 +2,21 @@ using System;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using InventoryTraker.Web.Core;
|
using InventoryTraker.Web.Core;
|
||||||
using Microsoft.AspNet.Identity;
|
using Microsoft.AspNet.Identity;
|
||||||
|
using Microsoft.AspNet.Identity.EntityFramework;
|
||||||
using Microsoft.AspNet.Identity.Owin;
|
using Microsoft.AspNet.Identity.Owin;
|
||||||
using Microsoft.Owin.Security.DataProtection;
|
using Microsoft.Owin.Security.DataProtection;
|
||||||
|
|
||||||
namespace InventoryTraker.Web.Identity
|
namespace InventoryTraker.Web.Identity
|
||||||
{
|
{
|
||||||
|
public class ApplicationRoleManager : RoleManager<IdentityRole>
|
||||||
|
{
|
||||||
|
public const string AdminRoleName = "Admin";
|
||||||
|
|
||||||
|
public ApplicationRoleManager(IRoleStore<IdentityRole, string> store) : base(store)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class ApplicationUserManager : UserManager<User>
|
public class ApplicationUserManager : UserManager<User>
|
||||||
{
|
{
|
||||||
public ApplicationUserManager(IUserStore<User> store, IDataProtectionProvider dataProtectionProvider)
|
public ApplicationUserManager(IUserStore<User> store, IDataProtectionProvider dataProtectionProvider)
|
||||||
|
|||||||
@@ -14,6 +14,34 @@ namespace InventoryTraker.Web.Migrations
|
|||||||
{
|
{
|
||||||
public static class SeedData
|
public static class SeedData
|
||||||
{
|
{
|
||||||
|
|
||||||
|
public static void AddAdminRole()
|
||||||
|
{
|
||||||
|
using (var context = new AppDbContext())
|
||||||
|
AddAdminRole(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddAdminRole(AppDbContext context)
|
||||||
|
{
|
||||||
|
var manager = new ApplicationRoleManager(new RoleStore<IdentityRole>(context));
|
||||||
|
if (!manager.RoleExists(ApplicationRoleManager.AdminRoleName))
|
||||||
|
{
|
||||||
|
var result = manager.Create(new IdentityRole(ApplicationRoleManager.AdminRoleName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no users are admins, make them all!
|
||||||
|
var adminRole = manager.Roles.First(r => r.Name == ApplicationRoleManager.AdminRoleName);
|
||||||
|
var userManager = new ApplicationUserManager(new UserStore<User>(context), null);
|
||||||
|
var admins = userManager.Users.Where(u => u.Roles.Any(r => r.RoleId == adminRole.Id));
|
||||||
|
if (!admins.Any())
|
||||||
|
{
|
||||||
|
foreach (var user in userManager.Users.ToList())
|
||||||
|
{
|
||||||
|
userManager.AddToRole(user.Id, ApplicationRoleManager.AdminRoleName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void Init()
|
public static void Init()
|
||||||
{
|
{
|
||||||
using (var context = new AppDbContext())
|
using (var context = new AppDbContext())
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ namespace InventoryTraker.Web.Models
|
|||||||
[RegularExpression(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", ErrorMessage = "Must be an email address")]
|
[RegularExpression(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", ErrorMessage = "Must be an email address")]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
//[Required]
|
||||||
|
public bool Administrator { get; set; }
|
||||||
|
|
||||||
[DataType(DataType.Password)]
|
[DataType(DataType.Password)]
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ namespace InventoryTraker.Web.Models
|
|||||||
[RegularExpression(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", ErrorMessage = "Must be an email address")]
|
[RegularExpression(@"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", ErrorMessage = "Must be an email address")]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public bool Administrator { get; set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"UserName: {UserName}, email: {Email}";
|
return $"UserName: {UserName}, email: {Email}";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>@ViewBag.Title - Inventory Traker</title>
|
<title>@ViewBag.Title - Inventory Traker</title>
|
||||||
@Styles.Render("~/Content/all-styles")
|
@Styles.Render("~/Content/all-styles")
|
||||||
@RenderSection("Styles", required: false)
|
@RenderSection("Styles", false)
|
||||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||||
<!--[if lt IE 9]>
|
<!--[if lt IE 9]>
|
||||||
@@ -23,11 +23,19 @@
|
|||||||
@RenderBody()
|
@RenderBody()
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (Request.IsAuthenticated)
|
||||||
|
{
|
||||||
|
<footer class="footer hidden-print">
|
||||||
|
<div class="container">
|
||||||
|
<p>Inventory Traker © 2016 Kolpack Software Consulting LLC</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
<!-- /#page-wrapper -->
|
<!-- /#page-wrapper -->
|
||||||
</div>
|
</div>
|
||||||
<!-- /#wrapper -->
|
<!-- /#wrapper -->
|
||||||
@Scripts.Render("~/js/all-javascript")
|
@Scripts.Render("~/js/all-javascript")
|
||||||
@RenderSection("Scripts", required: false)
|
@RenderSection("Scripts", false)
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
@using InventoryTraker.Web.Identity
|
||||||
|
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||||
<!-- Brand and toggle get grouped for better mobile display -->
|
<!-- Brand and toggle get grouped for better mobile display -->
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
|
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
|
||||||
@@ -7,13 +8,16 @@
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="~/">Inventory Traker</a>
|
<a class="navbar-brand" href="~/"><i class="fa fa-fw fa-cubes"></i> Inventory Traker</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Top Menu Items -->
|
<!-- Top Menu Items -->
|
||||||
<ul class="nav navbar-right top-nav">
|
<ul class="nav navbar-right top-nav">
|
||||||
|
@if (User.IsInRole(ApplicationRoleManager.AdminRoleName))
|
||||||
|
{
|
||||||
<li>
|
<li>
|
||||||
<a href="@(Html.BuildUrlFromExpression<UserController>(c => c.Index()))"><i class="fa fa-fw fa-users"></i> Users</a>
|
<a href="@(Html.BuildUrlFromExpression<UserController>(c => c.Index()))"><i class="fa fa-fw fa-users"></i> Users</a>
|
||||||
</li>
|
</li>
|
||||||
|
}
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-user"></i> @User.Identity.Name <b class="caret"></b></a>
|
<a href="" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-user"></i> @User.Identity.Name <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||||
<!-- Brand and toggle get grouped for better mobile display -->
|
<!-- Brand and toggle get grouped for better mobile display -->
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<a class="navbar-brand" href="~/">Inventory Traker</a>
|
<a class="navbar-brand" href="~/"><i class="fa fa-fw fa-cubes"></i> Inventory Traker</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -10,9 +10,31 @@ body {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
/* Set the fixed height of the footer here */
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
#page-wrapper {
|
#page-wrapper {
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
padding-bottom: 60px;
|
||||||
|
margin-bottom: -60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand i {
|
||||||
|
color:palegreen
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand:link,
|
||||||
|
.navbar-brand:visited,
|
||||||
|
.navbar-brand:hover,
|
||||||
|
.navbar-brand:active {
|
||||||
|
font-weight: bolder;
|
||||||
|
color: #92B9BD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar.navbar-fixed-top {
|
.navbar.navbar-fixed-top {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<th class="control-column"></th>
|
<th class="control-column"></th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
|
<th>Administrator</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -11,6 +12,10 @@
|
|||||||
<td><a href="" ng-click="vm.edit(user)"><i class="fa fa-edit"></i></a></td>
|
<td><a href="" ng-click="vm.edit(user)"><i class="fa fa-edit"></i></a></td>
|
||||||
<td>{{user.userName}}</td>
|
<td>{{user.userName}}</td>
|
||||||
<td>{{user.email}}</td>
|
<td>{{user.email}}</td>
|
||||||
|
<td>
|
||||||
|
<i class="fa fa-check-square-o" ng-show="user.administrator"></i>
|
||||||
|
<i class="fa fa-square-o" ng-show="!user.administrator"></i>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
|
|
||||||
function edit(existingUser, editedUser) {
|
function edit(existingUser, editedUser) {
|
||||||
return $http.post("/User/Edit", editedUser)
|
return $http.post("/User/Edit", editedUser)
|
||||||
.success(function(user) {
|
.success(function(data) {
|
||||||
angular.copy(user, existingUser);
|
angular.copy(data, existingUser);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user