support in-home easy password
This commit is contained in:
parent
82fe244fa1
commit
a6145e54d9
|
@ -11,7 +11,6 @@ using MediaBrowser.Model.Connect;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Users;
|
using MediaBrowser.Model.Users;
|
||||||
using ServiceStack;
|
using ServiceStack;
|
||||||
using ServiceStack.Text.Controller;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -148,6 +147,32 @@ namespace MediaBrowser.Api
|
||||||
public bool ResetPassword { get; set; }
|
public bool ResetPassword { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class UpdateUserEasyPassword
|
||||||
|
/// </summary>
|
||||||
|
[Route("/Users/{Id}/EasyPassword", "POST", Summary = "Updates a user's easy password")]
|
||||||
|
[Authenticated]
|
||||||
|
public class UpdateUserEasyPassword : IReturnVoid
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the id.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The id.</value>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the new password.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The new password.</value>
|
||||||
|
public string NewPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether [reset password].
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if [reset password]; otherwise, <c>false</c>.</value>
|
||||||
|
public bool ResetPassword { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class UpdateUser
|
/// Class UpdateUser
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -410,6 +435,8 @@ namespace MediaBrowser.Api
|
||||||
|
|
||||||
public async Task PostAsync(UpdateUserPassword request)
|
public async Task PostAsync(UpdateUserPassword request)
|
||||||
{
|
{
|
||||||
|
AssertCanUpdateUser(request.Id);
|
||||||
|
|
||||||
var user = _userManager.GetUserById(request.Id);
|
var user = _userManager.GetUserById(request.Id);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
|
@ -434,6 +461,33 @@ namespace MediaBrowser.Api
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Post(UpdateUserEasyPassword request)
|
||||||
|
{
|
||||||
|
var task = PostAsync(request);
|
||||||
|
Task.WaitAll(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PostAsync(UpdateUserEasyPassword request)
|
||||||
|
{
|
||||||
|
AssertCanUpdateUser(request.Id);
|
||||||
|
|
||||||
|
var user = _userManager.GetUserById(request.Id);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.ResetPassword)
|
||||||
|
{
|
||||||
|
await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _userManager.ChangeEasyPassword(user, request.NewPassword).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Posts the specified request.
|
/// Posts the specified request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -449,7 +503,9 @@ namespace MediaBrowser.Api
|
||||||
{
|
{
|
||||||
// We need to parse this manually because we told service stack not to with IRequiresRequestStream
|
// We need to parse this manually because we told service stack not to with IRequiresRequestStream
|
||||||
// https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
|
// https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
|
||||||
var id = new Guid(GetPathValue(1));
|
var id = GetPathValue(1);
|
||||||
|
|
||||||
|
AssertCanUpdateUser(id);
|
||||||
|
|
||||||
var dtoUser = request;
|
var dtoUser = request;
|
||||||
|
|
||||||
|
@ -499,11 +555,29 @@ namespace MediaBrowser.Api
|
||||||
|
|
||||||
public void Post(UpdateUserConfiguration request)
|
public void Post(UpdateUserConfiguration request)
|
||||||
{
|
{
|
||||||
|
AssertCanUpdateUser(request.Id);
|
||||||
|
|
||||||
var task = _userManager.UpdateConfiguration(request.Id, request);
|
var task = _userManager.UpdateConfiguration(request.Id, request);
|
||||||
|
|
||||||
Task.WaitAll(task);
|
Task.WaitAll(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AssertCanUpdateUser(string userId)
|
||||||
|
{
|
||||||
|
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||||
|
|
||||||
|
// If they're going to update the record of another user, they must be an administrator
|
||||||
|
if (!string.Equals(userId, auth.UserId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var authenticatedUser = _userManager.GetUserById(auth.UserId);
|
||||||
|
|
||||||
|
if (!authenticatedUser.Policy.IsAdministrator)
|
||||||
|
{
|
||||||
|
throw new SecurityException("Unauthorized access.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Post(UpdateUserPolicy request)
|
public void Post(UpdateUserPolicy request)
|
||||||
{
|
{
|
||||||
var task = UpdateUserPolicy(request);
|
var task = UpdateUserPolicy(request);
|
||||||
|
|
|
@ -50,7 +50,7 @@ namespace MediaBrowser.Common.Implementations.Networking
|
||||||
{
|
{
|
||||||
NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged;
|
NetworkChange.NetworkAddressChanged -= NetworkChange_NetworkAddressChanged;
|
||||||
NetworkChange.NetworkAvailabilityChanged -= NetworkChange_NetworkAvailabilityChanged;
|
NetworkChange.NetworkAvailabilityChanged -= NetworkChange_NetworkAvailabilityChanged;
|
||||||
|
|
||||||
NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged;
|
NetworkChange.NetworkAddressChanged += NetworkChange_NetworkAddressChanged;
|
||||||
NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
|
NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
|
||||||
}
|
}
|
||||||
|
@ -106,6 +106,11 @@ namespace MediaBrowser.Common.Implementations.Networking
|
||||||
// Private address space:
|
// Private address space:
|
||||||
// http://en.wikipedia.org/wiki/Private_network
|
// http://en.wikipedia.org/wiki/Private_network
|
||||||
|
|
||||||
|
if (endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Is172AddressPrivate(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
// If url was requested with computer name, we may see this
|
// If url was requested with computer name, we may see this
|
||||||
|
@ -114,11 +119,23 @@ namespace MediaBrowser.Common.Implementations.Networking
|
||||||
endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) ||
|
endpoint.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||||
endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
|
endpoint.StartsWith("127.", StringComparison.OrdinalIgnoreCase) ||
|
||||||
endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase) ||
|
endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase) ||
|
||||||
endpoint.StartsWith("192.", StringComparison.OrdinalIgnoreCase) ||
|
endpoint.StartsWith("192.168", StringComparison.OrdinalIgnoreCase) ||
|
||||||
endpoint.StartsWith("172.", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase);
|
endpoint.StartsWith("169.", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool Is172AddressPrivate(string endpoint)
|
||||||
|
{
|
||||||
|
for (var i = 16; i <= 31; i++)
|
||||||
|
{
|
||||||
|
if (endpoint.StartsWith("172." + i.ToString(CultureInfo.InvariantCulture) + ".", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsInLocalNetwork(string endpoint)
|
public bool IsInLocalNetwork(string endpoint)
|
||||||
{
|
{
|
||||||
return IsInLocalNetworkInternal(endpoint, true);
|
return IsInLocalNetworkInternal(endpoint, true);
|
||||||
|
@ -175,7 +192,7 @@ namespace MediaBrowser.Common.Implementations.Networking
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IPAddress> GetIpAddresses(string hostName)
|
public IEnumerable<IPAddress> GetIpAddresses(string hostName)
|
||||||
{
|
{
|
||||||
return Dns.GetHostAddresses(hostName);
|
return Dns.GetHostAddresses(hostName);
|
||||||
|
|
|
@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Entities
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The password.</value>
|
/// <value>The password.</value>
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
public string LocalPassword { get; set; }
|
public string EasyPassword { get; set; }
|
||||||
|
|
||||||
public string ConnectUserName { get; set; }
|
public string ConnectUserName { get; set; }
|
||||||
public string ConnectUserId { get; set; }
|
public string ConnectUserId { get; set; }
|
||||||
|
|
|
@ -117,6 +117,13 @@ namespace MediaBrowser.Controller.Library
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task ResetPassword(User user);
|
Task ResetPassword(User user);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the easy password.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
Task ResetEasyPassword(User user);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Changes the password.
|
/// Changes the password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -125,6 +132,14 @@ namespace MediaBrowser.Controller.Library
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task ChangePassword(User user, string newPasswordSha1);
|
Task ChangePassword(User user, string newPasswordSha1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the easy password.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <param name="newPasswordSha1">The new password sha1.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
Task ChangeEasyPassword(User user, string newPasswordSha1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user dto.
|
/// Gets the user dto.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -66,6 +66,12 @@ namespace MediaBrowser.Model.Dto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if this instance has configured password; otherwise, <c>false</c>.</value>
|
/// <value><c>true</c> if this instance has configured password; otherwise, <c>false</c>.</value>
|
||||||
public bool HasConfiguredPassword { get; set; }
|
public bool HasConfiguredPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether this instance has configured easy password.
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if this instance has configured easy password; otherwise, <c>false</c>.</value>
|
||||||
|
public bool HasConfiguredEasyPassword { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the last login date.
|
/// Gets or sets the last login date.
|
||||||
|
|
|
@ -275,9 +275,9 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||||
|
|
||||||
private string GetLocalPasswordHash(User user)
|
private string GetLocalPasswordHash(User user)
|
||||||
{
|
{
|
||||||
return string.IsNullOrEmpty(user.LocalPassword)
|
return string.IsNullOrEmpty(user.EasyPassword)
|
||||||
? GetSha1String(string.Empty)
|
? GetSha1String(string.Empty)
|
||||||
: user.LocalPassword;
|
: user.EasyPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPasswordEmpty(string passwordHash)
|
private bool IsPasswordEmpty(string passwordHash)
|
||||||
|
@ -355,18 +355,20 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||||
|
|
||||||
var passwordHash = GetPasswordHash(user);
|
var passwordHash = GetPasswordHash(user);
|
||||||
|
|
||||||
var hasConfiguredDefaultPassword = !IsPasswordEmpty(passwordHash);
|
var hasConfiguredPassword = !IsPasswordEmpty(passwordHash);
|
||||||
|
var hasConfiguredEasyPassword = !IsPasswordEmpty(GetLocalPasswordHash(user));
|
||||||
|
|
||||||
var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||||
!IsPasswordEmpty(GetLocalPasswordHash(user)) :
|
hasConfiguredEasyPassword :
|
||||||
hasConfiguredDefaultPassword;
|
hasConfiguredPassword;
|
||||||
|
|
||||||
var dto = new UserDto
|
var dto = new UserDto
|
||||||
{
|
{
|
||||||
Id = user.Id.ToString("N"),
|
Id = user.Id.ToString("N"),
|
||||||
Name = user.Name,
|
Name = user.Name,
|
||||||
HasPassword = hasPassword,
|
HasPassword = hasPassword,
|
||||||
HasConfiguredPassword = hasConfiguredDefaultPassword,
|
HasConfiguredPassword = hasConfiguredPassword,
|
||||||
|
HasConfiguredEasyPassword = hasConfiguredEasyPassword,
|
||||||
LastActivityDate = user.LastActivityDate,
|
LastActivityDate = user.LastActivityDate,
|
||||||
LastLoginDate = user.LastLoginDate,
|
LastLoginDate = user.LastLoginDate,
|
||||||
Configuration = user.Configuration,
|
Configuration = user.Configuration,
|
||||||
|
@ -613,18 +615,11 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||||
return ChangePassword(user, GetSha1String(string.Empty));
|
return ChangePassword(user, GetSha1String(string.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Task ResetEasyPassword(User user)
|
||||||
/// Changes the password.
|
{
|
||||||
/// </summary>
|
return ChangeEasyPassword(user, GetSha1String(string.Empty));
|
||||||
/// <param name="user">The user.</param>
|
}
|
||||||
/// <param name="newPasswordSha1">The new password sha1.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
/// <exception cref="System.ArgumentNullException">
|
|
||||||
/// user
|
|
||||||
/// or
|
|
||||||
/// newPassword
|
|
||||||
/// </exception>
|
|
||||||
/// <exception cref="System.ArgumentException">Passwords for guests cannot be changed.</exception>
|
|
||||||
public async Task ChangePassword(User user, string newPasswordSha1)
|
public async Task ChangePassword(User user, string newPasswordSha1)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
|
@ -648,6 +643,29 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||||
EventHelper.FireEventIfNotNull(UserPasswordChanged, this, new GenericEventArgs<User>(user), _logger);
|
EventHelper.FireEventIfNotNull(UserPasswordChanged, this, new GenericEventArgs<User>(user), _logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ChangeEasyPassword(User user, string newPasswordSha1)
|
||||||
|
{
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("user");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(newPasswordSha1))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("newPasswordSha1");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.ConnectLinkType.HasValue && user.ConnectLinkType.Value == UserLinkType.Guest)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Passwords for guests cannot be changed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
user.EasyPassword = newPasswordSha1;
|
||||||
|
|
||||||
|
await UpdateUser(user).ConfigureAwait(false);
|
||||||
|
|
||||||
|
EventHelper.FireEventIfNotNull(UserPasswordChanged, this, new GenericEventArgs<User>(user), _logger);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instantiates the new user.
|
/// Instantiates the new user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -14,9 +14,12 @@
|
||||||
"FileReadError": "An error occurred while reading the file.",
|
"FileReadError": "An error occurred while reading the file.",
|
||||||
"DeleteUser": "Delete User",
|
"DeleteUser": "Delete User",
|
||||||
"DeleteUserConfirmation": "Are you sure you wish to delete this user?",
|
"DeleteUserConfirmation": "Are you sure you wish to delete this user?",
|
||||||
"PasswordResetHeader": "Password Reset",
|
"PasswordResetHeader": "Reset Password",
|
||||||
"PasswordResetComplete": "The password has been reset.",
|
"PasswordResetComplete": "The password has been reset.",
|
||||||
|
"PinCodeResetComplete": "The pin code has been reset.",
|
||||||
"PasswordResetConfirmation": "Are you sure you wish to reset the password?",
|
"PasswordResetConfirmation": "Are you sure you wish to reset the password?",
|
||||||
|
"PinCodeResetConfirmation": "Are you sure you wish to reset the pin code?",
|
||||||
|
"HeaderPinCodeReset": "Reset Pin Code",
|
||||||
"PasswordSaved": "Password saved.",
|
"PasswordSaved": "Password saved.",
|
||||||
"PasswordMatchError": "Password and password confirmation must match.",
|
"PasswordMatchError": "Password and password confirmation must match.",
|
||||||
"OptionRelease": "Official Release",
|
"OptionRelease": "Official Release",
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
"HeaderVideo": "Video",
|
"HeaderVideo": "Video",
|
||||||
"HeaderPaths": "Paths",
|
"HeaderPaths": "Paths",
|
||||||
"CategorySync": "Sync",
|
"CategorySync": "Sync",
|
||||||
|
"HeaderEasyPinCode": "Easy Pin Code",
|
||||||
"RegisterWithPayPal": "Register with PayPal",
|
"RegisterWithPayPal": "Register with PayPal",
|
||||||
"HeaderSyncRequiresSupporterMembership": "Sync Requires a Supporter Membership",
|
"HeaderSyncRequiresSupporterMembership": "Sync Requires a Supporter Membership",
|
||||||
"HeaderEnjoyDayTrial": "Enjoy a 14 Day Free Trial",
|
"HeaderEnjoyDayTrial": "Enjoy a 14 Day Free Trial",
|
||||||
|
@ -1132,11 +1133,14 @@
|
||||||
"LabelDisplayFoldersView": "Display a folders view to show plain media folders",
|
"LabelDisplayFoldersView": "Display a folders view to show plain media folders",
|
||||||
"ViewTypeLiveTvRecordingGroups": "Recordings",
|
"ViewTypeLiveTvRecordingGroups": "Recordings",
|
||||||
"ViewTypeLiveTvChannels": "Channels",
|
"ViewTypeLiveTvChannels": "Channels",
|
||||||
"LabelAllowLocalAccessWithoutPassword": "Allow local access without a password",
|
"LabelEasyPinCode": "Easy pin code:",
|
||||||
"LabelAllowLocalAccessWithoutPasswordHelp": "When enabled, a password will not be required when signing in from within your home network.",
|
"EasyPasswordHelp": "Your easy pin code is used for offline access with supported Media Browser apps, and can also be used for easy in-network sign in.",
|
||||||
|
"LabelInNetworkSignInWithEasyPassword": "Enable in-network sign in with my easy pin code",
|
||||||
|
"LabelInNetworkSignInWithEasyPasswordHelp": "If enabled, you'll be able to use your easy pin code to sign in to Media Browser apps from inside your home network. Your regular password will only be needed away from home. If the pin code is left blank, you won't need a password within your home network.",
|
||||||
"HeaderPassword": "Password",
|
"HeaderPassword": "Password",
|
||||||
"HeaderLocalAccess": "Local Access",
|
"HeaderLocalAccess": "Local Access",
|
||||||
"HeaderViewOrder": "View Order",
|
"HeaderViewOrder": "View Order",
|
||||||
|
"ButtonResetEasyPassword": "Reset easy pin code",
|
||||||
"LabelSelectUserViewOrder": "Choose the order your views will be displayed in within Media Browser apps",
|
"LabelSelectUserViewOrder": "Choose the order your views will be displayed in within Media Browser apps",
|
||||||
"LabelMetadataRefreshMode": "Metadata refresh mode:",
|
"LabelMetadataRefreshMode": "Metadata refresh mode:",
|
||||||
"LabelImageRefreshMode": "Image refresh mode:",
|
"LabelImageRefreshMode": "Image refresh mode:",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user