update mobile response base format
This commit is contained in:
parent
89ef87fb18
commit
ad9694f85d
|
|
@ -1,5 +1,6 @@
|
||||||
using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems;
|
using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems;
|
||||||
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Common;
|
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Common;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
|
||||||
namespace OnlineSalesAutoCrop.CoreAPI.Models.Requests.Integrations;
|
namespace OnlineSalesAutoCrop.CoreAPI.Models.Requests.Integrations;
|
||||||
|
|
@ -11,23 +12,30 @@ public class IntegrationLoginRequest : AuthLoginRequest
|
||||||
|
|
||||||
public class IntegrationRefreshTokenRequest
|
public class IntegrationRefreshTokenRequest
|
||||||
{
|
{
|
||||||
|
[Required]
|
||||||
public string RefreshToken { get; set; }
|
public string RefreshToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InsertRefreshTokenRequest : RefreshToken
|
public class InsertRefreshTokenRequest : RefreshToken
|
||||||
{
|
{
|
||||||
|
[Required]
|
||||||
public string RefreshToken { get; set; }
|
public string RefreshToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RevokedRefreshTokenRequest
|
public class RevokedRefreshTokenRequest
|
||||||
{
|
{
|
||||||
|
[Required]
|
||||||
public string RefreshToken { get; set; }
|
public string RefreshToken { get; set; }
|
||||||
|
[Required]
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GenerateRefreshTokenRequest
|
public class GenerateRefreshTokenRequest
|
||||||
{
|
{
|
||||||
|
[Required]
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
|
[Required]
|
||||||
public string IpAddress { get; set; }
|
public string IpAddress { get; set; }
|
||||||
|
[Required]
|
||||||
public string RawRefreshToken { get; set; }
|
public string RawRefreshToken { get; set; }
|
||||||
}
|
}
|
||||||
|
|
@ -11,12 +11,19 @@ public class IntegrationLoginResponse : ResponseBase
|
||||||
public DateTime AccessTokenExpiry { get; set; }
|
public DateTime AccessTokenExpiry { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class IntegrationRrefreshTokenResponse : ResponseBase
|
public class IntegrationRrefreshTokenResponse : ResponseBase
|
||||||
{
|
{
|
||||||
public string AccessToken { get; set; } = string.Empty;
|
public string AccessToken { get; set; } = string.Empty;
|
||||||
public string RefreshToken { get; set; } = string.Empty;
|
public string RefreshToken { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class MobileRrefreshTokenResponse
|
||||||
|
{
|
||||||
|
public string AccessToken { get; set; } = string.Empty;
|
||||||
|
public string RefreshToken { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public class RefreshTokenResponse : ResponseBase
|
public class RefreshTokenResponse : ResponseBase
|
||||||
{
|
{
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ using System;
|
||||||
|
|
||||||
namespace OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp;
|
namespace OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp;
|
||||||
|
|
||||||
public class AppAuthUserResponse : ResponseBase
|
public class AppAuthUserResponse
|
||||||
{
|
{
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public string LoginId { get; set; }
|
public string LoginId { get; set; }
|
||||||
|
|
@ -26,14 +26,14 @@ public class AppAuthUserResponse : ResponseBase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class AuthLogoutResponse : ResponseBase
|
public class AuthLogoutResponse
|
||||||
{
|
{
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public string LoginId { get; set; }
|
public string LoginId { get; set; }
|
||||||
public bool IsSuccess { get; set; }
|
public bool IsSuccess { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppChangePasswordResponse : ResponseBase
|
public class AppChangePasswordResponse
|
||||||
{
|
{
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
public string LoginId { get; set; }
|
public string LoginId { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,21 @@ namespace OnlineSalesAutoCrop.CoreAPI.Models.Responses
|
||||||
public List<string> ReturnMessage { get; set; } = [];
|
public List<string> ReturnMessage { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class TotalRowsResponseBase : ResponseBase
|
public class MobileResponseBase<T>
|
||||||
|
{
|
||||||
|
public int ReturnStatus { get; set; }
|
||||||
|
public List<string> ReturnMessage { get; set; } = [];
|
||||||
|
public T? Data { get; set; }
|
||||||
|
|
||||||
|
public static MobileResponseBase<T> Success(T data, string message = "Success") =>
|
||||||
|
new() { ReturnStatus = 200, ReturnMessage = [message], Data = data };
|
||||||
|
|
||||||
|
public static MobileResponseBase<T> Failure(string message, int status = 400 ) =>
|
||||||
|
new() { ReturnStatus = status, ReturnMessage = [message], Data = default };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public abstract class TotalRowsResponseBase : ResponseBase
|
||||||
{
|
{
|
||||||
public int TotalRows { get; set; }
|
public int TotalRows { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ using OnlineSalesAutoCrop.CoreAPI.Models.Objects.Systems;
|
||||||
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Common;
|
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Common;
|
||||||
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Integrations;
|
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.Integrations;
|
||||||
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.MobileApp;
|
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.MobileApp;
|
||||||
|
using OnlineSalesAutoCrop.CoreAPI.Models.Responses;
|
||||||
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
|
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
|
||||||
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp;
|
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp;
|
||||||
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Systems;
|
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Systems;
|
||||||
|
|
@ -74,16 +75,14 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.LoginId))
|
if (string.IsNullOrEmpty(request.LoginId))
|
||||||
{
|
{
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status417ExpectationFailed;
|
string message = "Login ID is required.";
|
||||||
loginReponse.ReturnMessage.Add("Login ID is required.");
|
return BadRequest(MobileResponseBase<AppAuthUserResponse>.Failure(message, StatusCodes.Status417ExpectationFailed));
|
||||||
return StatusCode(StatusCodes.Status417ExpectationFailed, loginReponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(request.Password))
|
if (string.IsNullOrEmpty(request.Password))
|
||||||
{
|
{
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status417ExpectationFailed;
|
string message = "Password is required.";
|
||||||
loginReponse.ReturnMessage.Add("Password is required.");
|
return BadRequest(MobileResponseBase<AppAuthUserResponse>.Failure(message, StatusCodes.Status417ExpectationFailed));
|
||||||
return StatusCode(StatusCodes.Status417ExpectationFailed, loginReponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -104,38 +103,29 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers
|
||||||
|
|
||||||
if (loginReponse == null || loginReponse.UserId == 0)
|
if (loginReponse == null || loginReponse.UserId == 0)
|
||||||
{
|
{
|
||||||
loginReponse.LoginStatus = EnumLoginStatus.Error;
|
string message = "Login ID/Password is invalid.\" : \"You are not Authorized to login into the System.";
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status403Forbidden;
|
return Unauthorized(MobileResponseBase<AppAuthUserResponse>.Failure(message, StatusCodes.Status403Forbidden));
|
||||||
loginReponse.ReturnMessage.Add(checkPwd ? "Login ID/Password is invalid." : "You are not Authorized to login into the System");
|
|
||||||
return StatusCode(StatusCodes.Status417ExpectationFailed, loginReponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (loginReponse.LoginStatus == EnumLoginStatus.Unsuccessful)
|
if (loginReponse.LoginStatus == EnumLoginStatus.Unsuccessful)
|
||||||
{
|
{
|
||||||
loginReponse.LoginStatus = loginReponse.LoginStatus;
|
string message = checkPwd ? $"Login ID/Password is invalid for {request.LoginId}." : "You are not Authorized to login into the System";
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status403Forbidden;
|
return Unauthorized(MobileResponseBase<AppAuthUserResponse>.Failure(message, StatusCodes.Status403Forbidden));
|
||||||
loginReponse.ReturnMessage.Add(checkPwd ? $"Login ID/Password is invalid for {request.LoginId}." : "You are not Authorized to login into the System");
|
|
||||||
return StatusCode(StatusCodes.Status417ExpectationFailed, loginReponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (loginReponse.IsLocked)
|
if (loginReponse.IsLocked)
|
||||||
{
|
{
|
||||||
loginReponse.LoginStatus = loginReponse.LoginStatus;
|
string message =$"Your Account is locked . Please try again after few minutes";
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status403Forbidden;
|
return Unauthorized(MobileResponseBase<AppAuthUserResponse>.Failure(message, StatusCodes.Status403Forbidden));
|
||||||
loginReponse.ReturnMessage.Add($"Your Account is locked . Please try again after few minutes");
|
|
||||||
return StatusCode(StatusCodes.Status417ExpectationFailed, loginReponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loginReponse.IsAuthorized )
|
if (!loginReponse.IsAuthorized )
|
||||||
{
|
{
|
||||||
loginReponse.LoginStatus = loginReponse.LoginStatus;
|
string message = $"You are not Authorized to Login into the System, Please contact with System Administrator.";
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status403Forbidden;
|
return Unauthorized(MobileResponseBase<AppAuthUserResponse>.Failure(message, StatusCodes.Status403Forbidden));
|
||||||
loginReponse.ReturnMessage.Add("You are not Authorized to Login into the System, Please contact with System Administrator.");
|
|
||||||
return StatusCode(StatusCodes.Status417ExpectationFailed, loginReponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status200OK;
|
|
||||||
string pwdSecretKey = GlobalFunctions.ConvertFromBase64String(_appSettings.PwdSecretKey);
|
string pwdSecretKey = GlobalFunctions.ConvertFromBase64String(_appSettings.PwdSecretKey);
|
||||||
string userPwd = Ease.NetCore.Utility.Global.CipherFunctions.EncryptByAES(privateKey: pwdSecretKey, publicKey: pwdSecretKey, data: request.Password);
|
string userPwd = Ease.NetCore.Utility.Global.CipherFunctions.EncryptByAES(privateKey: pwdSecretKey, publicKey: pwdSecretKey, data: request.Password);
|
||||||
byte[] key = Encoding.ASCII.GetBytes(_appSettings.JwtCryptoKey);
|
byte[] key = Encoding.ASCII.GetBytes(_appSettings.JwtCryptoKey);
|
||||||
|
|
@ -168,29 +158,25 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers
|
||||||
//because cookie can not store more than 4KB data and we are storing this token in cookie for authentication
|
//because cookie can not store more than 4KB data and we are storing this token in cookie for authentication
|
||||||
if (userToken.Length >= 4096) //4Kb
|
if (userToken.Length >= 4096) //4Kb
|
||||||
{
|
{
|
||||||
loginReponse.LoginStatus = loginReponse.LoginStatus;
|
string message = $"Authentication Token is too large for cookie.";
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status431RequestHeaderFieldsTooLarge;
|
return StatusCode(StatusCodes.Status431RequestHeaderFieldsTooLarge, MobileResponseBase<AppAuthUserResponse>.Failure(message, StatusCodes.Status431RequestHeaderFieldsTooLarge));
|
||||||
loginReponse.ReturnMessage.Add("Authentication Token is too large for cookie.");
|
|
||||||
return StatusCode(StatusCodes.Status431RequestHeaderFieldsTooLarge, loginReponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loginReponse.ReturnStatus = loginReponse.ReturnStatus;
|
|
||||||
loginReponse.ReturnMessage = loginReponse.ReturnMessage;
|
|
||||||
loginReponse.LoginId = loginReponse.LoginId;
|
loginReponse.LoginId = loginReponse.LoginId;
|
||||||
loginReponse.AccessToken = userToken;
|
loginReponse.AccessToken = userToken;
|
||||||
loginReponse.RefreshToken = refreshToken.RefreshToken;
|
loginReponse.RefreshToken = refreshToken.RefreshToken;
|
||||||
loginReponse.AccessTokenExpiryTime = DateTime.UtcNow.AddHours(12);
|
loginReponse.AccessTokenExpiryTime = DateTime.UtcNow.AddHours(12);
|
||||||
loginReponse.RefreshTokenExpiryTime = refreshToken.ExpireTime;
|
loginReponse.RefreshTokenExpiryTime = refreshToken.ExpireTime;
|
||||||
return StatusCode(StatusCodes.Status200OK, loginReponse);
|
|
||||||
|
|
||||||
|
return Ok(MobileResponseBase<AppAuthUserResponse>.Success(loginReponse));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
string msg = $"{request?.LoginId}~{ipAddress}";
|
string msg = $"{request?.LoginId}~{ipAddress}";
|
||||||
_logger.LogError(exception: ex, message: msg);
|
_logger.LogError(exception: ex, message: msg);
|
||||||
loginReponse.ReturnStatus = StatusCodes.Status500InternalServerError;
|
string mesgae= ex.InnerException != null ? ex.InnerException.Message : ex.Message;
|
||||||
loginReponse.ReturnMessage.Add(ex.InnerException != null ? ex.InnerException.Message : ex.Message);
|
return StatusCode(StatusCodes.Status500InternalServerError, MobileResponseBase<AppAuthUserResponse>.Failure(mesgae, StatusCodes.Status500InternalServerError));
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, loginReponse);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,10 +192,10 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers
|
||||||
[HttpPost("RefreshToken")]
|
[HttpPost("RefreshToken")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[IgnoreAntiforgeryToken]
|
[IgnoreAntiforgeryToken]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IntegrationLoginResponse))]
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MobileRrefreshTokenResponse))]
|
||||||
public async Task<IActionResult> GetRefreshToken([FromBody] IntegrationRefreshTokenRequest request)
|
public async Task<IActionResult> GetRefreshToken([FromBody] IntegrationRefreshTokenRequest request)
|
||||||
{
|
{
|
||||||
IntegrationRrefreshTokenResponse response = new();
|
MobileRrefreshTokenResponse response = new();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var userRefreshToken = await _refreshTokenService.GetUserByRefreshTokenAsync(request.RefreshToken);
|
var userRefreshToken = await _refreshTokenService.GetUserByRefreshTokenAsync(request.RefreshToken);
|
||||||
|
|
@ -217,9 +203,7 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers
|
||||||
{
|
{
|
||||||
string msg = $"Refresh Token is not active: {request?.RefreshToken}";
|
string msg = $"Refresh Token is not active: {request?.RefreshToken}";
|
||||||
_logger.LogError(message: msg);
|
_logger.LogError(message: msg);
|
||||||
response.ReturnStatus = StatusCodes.Status401Unauthorized;
|
return Unauthorized( MobileResponseBase<AppAuthUserResponse>.Failure(msg, StatusCodes.Status401Unauthorized));
|
||||||
response.ReturnMessage.Add(msg);
|
|
||||||
return StatusCode(StatusCodes.Status401Unauthorized, response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] key = Encoding.ASCII.GetBytes(_appSettings.JwtCryptoKey);
|
byte[] key = Encoding.ASCII.GetBytes(_appSettings.JwtCryptoKey);
|
||||||
|
|
@ -246,16 +230,13 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers
|
||||||
response.AccessToken = userToken;
|
response.AccessToken = userToken;
|
||||||
response.RefreshToken = refreshToken;
|
response.RefreshToken = refreshToken;
|
||||||
|
|
||||||
return StatusCode(StatusCodes.Status200OK, response);
|
return Ok(MobileResponseBase<MobileRrefreshTokenResponse>.Success(response));
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
string msg = $"{request?.RefreshToken}";
|
string msg = $"{request?.RefreshToken}";
|
||||||
_logger.LogError(exception: ex, message: msg);
|
_logger.LogError(exception: ex, message: msg);
|
||||||
response.ReturnStatus = StatusCodes.Status500InternalServerError;
|
return StatusCode(StatusCodes.Status500InternalServerError, MobileResponseBase<AppAuthUserResponse>.Failure(ex.InnerException != null ? ex.InnerException.Message : ex.Message, StatusCodes.Status500InternalServerError));
|
||||||
response.ReturnMessage.Add(ex.InnerException != null ? ex.InnerException.Message : ex.Message);
|
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,19 +260,14 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers
|
||||||
{
|
{
|
||||||
string ipAddress = Request.HttpContext.GetIpAddress();
|
string ipAddress = Request.HttpContext.GetIpAddress();
|
||||||
response = await _service.LogoutAsync(request, ipAddress);
|
response = await _service.LogoutAsync(request, ipAddress);
|
||||||
|
return Ok(MobileResponseBase<AuthLogoutResponse>.Success(response, "Successfully Logout"));
|
||||||
response.ReturnStatus = StatusCodes.Status200OK;
|
|
||||||
response.ReturnMessage.Add("Successfully Logout");
|
|
||||||
return StatusCode(StatusCodes.Status200OK, response);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
string msg = $"Exception occur on logout {request?.LoginId}";
|
string msg = $"Exception occur on logout {request?.LoginId}";
|
||||||
_logger.LogError(exception: ex, message: msg);
|
_logger.LogError(exception: ex, message: msg);
|
||||||
response.ReturnStatus = StatusCodes.Status500InternalServerError;
|
return StatusCode(StatusCodes.Status500InternalServerError, MobileResponseBase<AppAuthUserResponse>.Failure(ex.InnerException != null ? ex.InnerException.Message : ex.Message, StatusCodes.Status500InternalServerError));
|
||||||
response.ReturnMessage.Add(ex.InnerException != null ? ex.InnerException.Message : ex.Message);
|
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,19 +291,14 @@ namespace OnlineSalesAutoCrop.CoreAPI.Controllers
|
||||||
{
|
{
|
||||||
string ipAddress = Request.HttpContext.GetIpAddress();
|
string ipAddress = Request.HttpContext.GetIpAddress();
|
||||||
response = await _service.ChangePasswordAsync(request, ipAddress);
|
response = await _service.ChangePasswordAsync(request, ipAddress);
|
||||||
|
return Ok(MobileResponseBase<AppChangePasswordResponse>.Success(response, "Successfully Changed the Password"));
|
||||||
response.ReturnStatus = StatusCodes.Status200OK;
|
|
||||||
response.ReturnMessage.Add("Successfully Changed the Password");
|
|
||||||
return StatusCode(StatusCodes.Status200OK, response);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
string msg = $"Exception occur on logout {request?.LoginId}";
|
string msg = $"Exception occur on logout {request?.LoginId}";
|
||||||
_logger.LogError(exception: ex, message: msg);
|
_logger.LogError(exception: ex, message: msg);
|
||||||
response.ReturnStatus = StatusCodes.Status500InternalServerError;
|
return StatusCode(StatusCodes.Status500InternalServerError, MobileResponseBase<AppAuthUserResponse>.Failure(ex.InnerException != null ? ex.InnerException.Message : ex.Message, StatusCodes.Status500InternalServerError));
|
||||||
response.ReturnMessage.Add(ex.InnerException != null ? ex.InnerException.Message : ex.Message);
|
|
||||||
return StatusCode(StatusCodes.Status500InternalServerError, response);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user