mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
feat(user): implement force password reset (#3572)
This commit is contained in:
@ -24,6 +24,8 @@ pub enum SetMetaDataRequest {
|
|||||||
ConfigureWoocom,
|
ConfigureWoocom,
|
||||||
SetupWoocomWebhook,
|
SetupWoocomWebhook,
|
||||||
IsMultipleConfiguration,
|
IsMultipleConfiguration,
|
||||||
|
#[serde(skip)]
|
||||||
|
IsChangePasswordRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
@ -110,6 +112,7 @@ pub enum GetMetaDataRequest {
|
|||||||
ConfigureWoocom,
|
ConfigureWoocom,
|
||||||
SetupWoocomWebhook,
|
SetupWoocomWebhook,
|
||||||
IsMultipleConfiguration,
|
IsMultipleConfiguration,
|
||||||
|
IsChangePasswordRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
@ -146,4 +149,5 @@ pub enum GetMetaDataResponse {
|
|||||||
ConfigureWoocom(bool),
|
ConfigureWoocom(bool),
|
||||||
SetupWoocomWebhook(bool),
|
SetupWoocomWebhook(bool),
|
||||||
IsMultipleConfiguration(bool),
|
IsMultipleConfiguration(bool),
|
||||||
|
IsChangePasswordRequired(bool),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -498,4 +498,5 @@ pub enum DashboardMetadata {
|
|||||||
ConfigureWoocom,
|
ConfigureWoocom,
|
||||||
SetupWoocomWebhook,
|
SetupWoocomWebhook,
|
||||||
IsMultipleConfiguration,
|
IsMultipleConfiguration,
|
||||||
|
IsChangePasswordRequired,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,7 +105,7 @@ impl DashboardMetadata {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
|
pub async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
|
||||||
conn: &PgPooledConn,
|
conn: &PgPooledConn,
|
||||||
user_id: String,
|
user_id: String,
|
||||||
merchant_id: String,
|
merchant_id: String,
|
||||||
@ -118,4 +118,20 @@ impl DashboardMetadata {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
|
||||||
|
conn: &PgPooledConn,
|
||||||
|
user_id: String,
|
||||||
|
merchant_id: String,
|
||||||
|
data_key: enums::DashboardMetadata,
|
||||||
|
) -> StorageResult<Self> {
|
||||||
|
generics::generic_delete_one_with_result::<<Self as HasTable>::Table, _, _>(
|
||||||
|
conn,
|
||||||
|
dsl::user_id
|
||||||
|
.eq(user_id)
|
||||||
|
.and(dsl::merchant_id.eq(merchant_id))
|
||||||
|
.and(dsl::data_key.eq(data_key)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,8 +8,9 @@ use error_stack::ResultExt;
|
|||||||
use masking::ExposeInterface;
|
use masking::ExposeInterface;
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
use router_env::env;
|
use router_env::env;
|
||||||
#[cfg(feature = "email")]
|
|
||||||
use router_env::logger;
|
use router_env::logger;
|
||||||
|
#[cfg(not(feature = "email"))]
|
||||||
|
use user_api::dashboard_metadata::SetMetaDataRequest;
|
||||||
|
|
||||||
use super::errors::{StorageErrorExt, UserErrors, UserResponse, UserResult};
|
use super::errors::{StorageErrorExt, UserErrors, UserResponse, UserResult};
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
@ -310,6 +311,20 @@ pub async fn change_password(
|
|||||||
.await
|
.await
|
||||||
.change_context(UserErrors::InternalServerError)?;
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "email"))]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
|
||||||
|
&user_from_token.user_id,
|
||||||
|
&user_from_token.merchant_id,
|
||||||
|
diesel_models::enums::DashboardMetadata::IsChangePasswordRequired,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| logger::error!("Error while deleting dashboard metadata {}", e))
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ApplicationResponse::StatusOk)
|
Ok(ApplicationResponse::StatusOk)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,8 +498,8 @@ pub async fn invite_user(
|
|||||||
.insert_user_role(UserRoleNew {
|
.insert_user_role(UserRoleNew {
|
||||||
user_id: new_user.get_user_id().to_owned(),
|
user_id: new_user.get_user_id().to_owned(),
|
||||||
merchant_id: user_from_token.merchant_id.clone(),
|
merchant_id: user_from_token.merchant_id.clone(),
|
||||||
role_id: request.role_id,
|
role_id: request.role_id.clone(),
|
||||||
org_id: user_from_token.org_id,
|
org_id: user_from_token.org_id.clone(),
|
||||||
status: invitation_status,
|
status: invitation_status,
|
||||||
created_by: user_from_token.user_id.clone(),
|
created_by: user_from_token.user_id.clone(),
|
||||||
last_modified_by: user_from_token.user_id,
|
last_modified_by: user_from_token.user_id,
|
||||||
@ -523,6 +538,20 @@ pub async fn invite_user(
|
|||||||
#[cfg(not(feature = "email"))]
|
#[cfg(not(feature = "email"))]
|
||||||
{
|
{
|
||||||
is_email_sent = false;
|
is_email_sent = false;
|
||||||
|
let invited_user_token = auth::UserFromToken {
|
||||||
|
user_id: new_user.get_user_id(),
|
||||||
|
merchant_id: user_from_token.merchant_id,
|
||||||
|
org_id: user_from_token.org_id,
|
||||||
|
role_id: request.role_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let set_metadata_request = SetMetaDataRequest::IsChangePasswordRequired;
|
||||||
|
dashboard_metadata::set_metadata(
|
||||||
|
state.clone(),
|
||||||
|
invited_user_token,
|
||||||
|
set_metadata_request,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ApplicationResponse::Json(user_api::InviteUserResponse {
|
Ok(ApplicationResponse::Json(user_api::InviteUserResponse {
|
||||||
@ -706,6 +735,17 @@ async fn handle_new_user_invitation(
|
|||||||
#[cfg(not(feature = "email"))]
|
#[cfg(not(feature = "email"))]
|
||||||
{
|
{
|
||||||
is_email_sent = false;
|
is_email_sent = false;
|
||||||
|
|
||||||
|
let invited_user_token = auth::UserFromToken {
|
||||||
|
user_id: new_user.get_user_id(),
|
||||||
|
merchant_id: user_from_token.merchant_id.clone(),
|
||||||
|
org_id: user_from_token.org_id.clone(),
|
||||||
|
role_id: request.role_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let set_metadata_request = SetMetaDataRequest::IsChangePasswordRequired;
|
||||||
|
dashboard_metadata::set_metadata(state.clone(), invited_user_token, set_metadata_request)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(InviteMultipleUserResponse {
|
Ok(InviteMultipleUserResponse {
|
||||||
|
|||||||
@ -105,6 +105,9 @@ fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult<types::Me
|
|||||||
api::SetMetaDataRequest::IsMultipleConfiguration => {
|
api::SetMetaDataRequest::IsMultipleConfiguration => {
|
||||||
Ok(types::MetaData::IsMultipleConfiguration(true))
|
Ok(types::MetaData::IsMultipleConfiguration(true))
|
||||||
}
|
}
|
||||||
|
api::SetMetaDataRequest::IsChangePasswordRequired => {
|
||||||
|
Ok(types::MetaData::IsChangePasswordRequired(true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +134,7 @@ fn parse_get_request(data_enum: api::GetMetaDataRequest) -> DBEnum {
|
|||||||
api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom,
|
api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom,
|
||||||
api::GetMetaDataRequest::SetupWoocomWebhook => DBEnum::SetupWoocomWebhook,
|
api::GetMetaDataRequest::SetupWoocomWebhook => DBEnum::SetupWoocomWebhook,
|
||||||
api::GetMetaDataRequest::IsMultipleConfiguration => DBEnum::IsMultipleConfiguration,
|
api::GetMetaDataRequest::IsMultipleConfiguration => DBEnum::IsMultipleConfiguration,
|
||||||
|
api::GetMetaDataRequest::IsChangePasswordRequired => DBEnum::IsChangePasswordRequired,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,6 +211,9 @@ fn into_response(
|
|||||||
DBEnum::IsMultipleConfiguration => Ok(api::GetMetaDataResponse::IsMultipleConfiguration(
|
DBEnum::IsMultipleConfiguration => Ok(api::GetMetaDataResponse::IsMultipleConfiguration(
|
||||||
data.is_some(),
|
data.is_some(),
|
||||||
)),
|
)),
|
||||||
|
DBEnum::IsChangePasswordRequired => Ok(api::GetMetaDataResponse::IsChangePasswordRequired(
|
||||||
|
data.is_some(),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,6 +527,17 @@ async fn insert_metadata(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
types::MetaData::IsChangePasswordRequired(data) => {
|
||||||
|
utils::insert_user_scoped_metadata_to_db(
|
||||||
|
state,
|
||||||
|
user.user_id,
|
||||||
|
user.merchant_id,
|
||||||
|
user.org_id,
|
||||||
|
metadata_key,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ pub trait DashboardMetadataInterface {
|
|||||||
&self,
|
&self,
|
||||||
metadata: storage::DashboardMetadataNew,
|
metadata: storage::DashboardMetadataNew,
|
||||||
) -> CustomResult<storage::DashboardMetadata, errors::StorageError>;
|
) -> CustomResult<storage::DashboardMetadata, errors::StorageError>;
|
||||||
|
|
||||||
async fn update_metadata(
|
async fn update_metadata(
|
||||||
&self,
|
&self,
|
||||||
user_id: Option<String>,
|
user_id: Option<String>,
|
||||||
@ -30,6 +31,7 @@ pub trait DashboardMetadataInterface {
|
|||||||
org_id: &str,
|
org_id: &str,
|
||||||
data_keys: Vec<enums::DashboardMetadata>,
|
data_keys: Vec<enums::DashboardMetadata>,
|
||||||
) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;
|
) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;
|
||||||
|
|
||||||
async fn find_merchant_scoped_dashboard_metadata(
|
async fn find_merchant_scoped_dashboard_metadata(
|
||||||
&self,
|
&self,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
@ -37,11 +39,18 @@ pub trait DashboardMetadataInterface {
|
|||||||
data_keys: Vec<enums::DashboardMetadata>,
|
data_keys: Vec<enums::DashboardMetadata>,
|
||||||
) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;
|
) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;
|
||||||
|
|
||||||
async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
|
async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
) -> CustomResult<bool, errors::StorageError>;
|
) -> CustomResult<bool, errors::StorageError>;
|
||||||
|
|
||||||
|
async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
merchant_id: &str,
|
||||||
|
data_key: enums::DashboardMetadata,
|
||||||
|
) -> CustomResult<storage::DashboardMetadata, errors::StorageError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@ -117,13 +126,13 @@ impl DashboardMetadataInterface for Store {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.into_report()
|
.into_report()
|
||||||
}
|
}
|
||||||
async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
|
async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
) -> CustomResult<bool, errors::StorageError> {
|
) -> CustomResult<bool, errors::StorageError> {
|
||||||
let conn = connection::pg_connection_write(self).await?;
|
let conn = connection::pg_connection_write(self).await?;
|
||||||
storage::DashboardMetadata::delete_user_scoped_dashboard_metadata_by_merchant_id(
|
storage::DashboardMetadata::delete_all_user_scoped_dashboard_metadata_by_merchant_id(
|
||||||
&conn,
|
&conn,
|
||||||
user_id.to_owned(),
|
user_id.to_owned(),
|
||||||
merchant_id.to_owned(),
|
merchant_id.to_owned(),
|
||||||
@ -132,6 +141,24 @@ impl DashboardMetadataInterface for Store {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
.into_report()
|
.into_report()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
merchant_id: &str,
|
||||||
|
data_key: enums::DashboardMetadata,
|
||||||
|
) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
|
||||||
|
let conn = connection::pg_connection_write(self).await?;
|
||||||
|
storage::DashboardMetadata::delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
|
||||||
|
&conn,
|
||||||
|
user_id.to_owned(),
|
||||||
|
merchant_id.to_owned(),
|
||||||
|
data_key,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
.into_report()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@ -267,7 +294,7 @@ impl DashboardMetadataInterface for MockDb {
|
|||||||
}
|
}
|
||||||
Ok(query_result)
|
Ok(query_result)
|
||||||
}
|
}
|
||||||
async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
|
async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
@ -294,4 +321,31 @@ impl DashboardMetadataInterface for MockDb {
|
|||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
merchant_id: &str,
|
||||||
|
data_key: enums::DashboardMetadata,
|
||||||
|
) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
|
||||||
|
let mut dashboard_metadata = self.dashboard_metadata.lock().await;
|
||||||
|
|
||||||
|
let index_to_remove = dashboard_metadata
|
||||||
|
.iter()
|
||||||
|
.position(|metadata_inner| {
|
||||||
|
metadata_inner
|
||||||
|
.user_id
|
||||||
|
.as_deref()
|
||||||
|
.map_or(false, |user_id_inner| user_id_inner == user_id)
|
||||||
|
&& metadata_inner.merchant_id == merchant_id
|
||||||
|
&& metadata_inner.data_key == data_key
|
||||||
|
})
|
||||||
|
.ok_or(errors::StorageError::ValueNotFound(
|
||||||
|
"No data found".to_string(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let deleted_value = dashboard_metadata.swap_remove(index_to_remove);
|
||||||
|
|
||||||
|
Ok(deleted_value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1964,6 +1964,7 @@ impl UserRoleInterface for KafkaStore {
|
|||||||
.update_user_role_by_user_id_merchant_id(user_id, merchant_id, update)
|
.update_user_role_by_user_id_merchant_id(user_id, merchant_id, update)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_user_role_by_user_id_merchant_id(
|
async fn delete_user_role_by_user_id_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
@ -2021,6 +2022,7 @@ impl DashboardMetadataInterface for KafkaStore {
|
|||||||
.find_user_scoped_dashboard_metadata(user_id, merchant_id, org_id, data_keys)
|
.find_user_scoped_dashboard_metadata(user_id, merchant_id, org_id, data_keys)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_merchant_scoped_dashboard_metadata(
|
async fn find_merchant_scoped_dashboard_metadata(
|
||||||
&self,
|
&self,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
@ -2032,13 +2034,28 @@ impl DashboardMetadataInterface for KafkaStore {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_user_scoped_dashboard_metadata_by_merchant_id(
|
async fn delete_all_user_scoped_dashboard_metadata_by_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
) -> CustomResult<bool, errors::StorageError> {
|
) -> CustomResult<bool, errors::StorageError> {
|
||||||
self.diesel_store
|
self.diesel_store
|
||||||
.delete_user_scoped_dashboard_metadata_by_merchant_id(user_id, merchant_id)
|
.delete_all_user_scoped_dashboard_metadata_by_merchant_id(user_id, merchant_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
|
||||||
|
&self,
|
||||||
|
user_id: &str,
|
||||||
|
merchant_id: &str,
|
||||||
|
data_key: enums::DashboardMetadata,
|
||||||
|
) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
|
||||||
|
self.diesel_store
|
||||||
|
.delete_user_scoped_dashboard_metadata_by_merchant_id_data_key(
|
||||||
|
user_id,
|
||||||
|
merchant_id,
|
||||||
|
data_key,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ pub enum MetaData {
|
|||||||
ConfigureWoocom(bool),
|
ConfigureWoocom(bool),
|
||||||
SetupWoocomWebhook(bool),
|
SetupWoocomWebhook(bool),
|
||||||
IsMultipleConfiguration(bool),
|
IsMultipleConfiguration(bool),
|
||||||
|
IsChangePasswordRequired(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&MetaData> for DBEnum {
|
impl From<&MetaData> for DBEnum {
|
||||||
@ -51,6 +52,7 @@ impl From<&MetaData> for DBEnum {
|
|||||||
MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom,
|
MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom,
|
||||||
MetaData::SetupWoocomWebhook(_) => Self::SetupWoocomWebhook,
|
MetaData::SetupWoocomWebhook(_) => Self::SetupWoocomWebhook,
|
||||||
MetaData::IsMultipleConfiguration(_) => Self::IsMultipleConfiguration,
|
MetaData::IsMultipleConfiguration(_) => Self::IsMultipleConfiguration,
|
||||||
|
MetaData::IsChangePasswordRequired(_) => Self::IsChangePasswordRequired,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -219,7 +219,9 @@ pub fn separate_metadata_type_based_on_scope(
|
|||||||
| DBEnum::ConfigureWoocom
|
| DBEnum::ConfigureWoocom
|
||||||
| DBEnum::SetupWoocomWebhook
|
| DBEnum::SetupWoocomWebhook
|
||||||
| DBEnum::IsMultipleConfiguration => merchant_scoped.push(key),
|
| DBEnum::IsMultipleConfiguration => merchant_scoped.push(key),
|
||||||
DBEnum::Feedback | DBEnum::ProdIntent => user_scoped.push(key),
|
DBEnum::Feedback | DBEnum::ProdIntent | DBEnum::IsChangePasswordRequired => {
|
||||||
|
user_scoped.push(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(merchant_scoped, user_scoped)
|
(merchant_scoped, user_scoped)
|
||||||
|
|||||||
Reference in New Issue
Block a user