User lib 1.0
This commit is contained in:
parent
ed455426dd
commit
c784889623
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -277,6 +277,17 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "err"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "git+https://git.franklinblanco.dev/franklinblanco/err.git#18cc77b6266d0fc90237a7ccb297d3eeb574f78a"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"sqlx",
|
||||||
|
"stdext",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -1374,6 +1385,12 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stdext"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f3b6b32ae82412fb897ef134867d53a294f57ba5b758f06d71e865352c3e207"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@ -1608,12 +1625,12 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
|
"err",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"ring",
|
"ring",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -18,4 +18,5 @@ ring = "0.16.20"
|
|||||||
data-encoding = "2.3.2"
|
data-encoding = "2.3.2"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
log = "0.4.19"
|
log = "0.4.19"
|
||||||
thiserror = "1.0.48"
|
|
||||||
|
err = { git = "https://git.franklinblanco.dev/franklinblanco/err.git" }
|
@ -1,39 +0,0 @@
|
|||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
/// Used to return a simple error from FromStr implementations.
|
|
||||||
#[error("Error parsing string into value")]
|
|
||||||
FromStrError,
|
|
||||||
/// Every error that is returned from a DAO operation.
|
|
||||||
#[error("Error from the Database: {0}")]
|
|
||||||
DatabaseError(#[from] sqlx::Error),
|
|
||||||
/// A vec of ValidationErrors
|
|
||||||
#[error("Validation Errors: {0:?}")]
|
|
||||||
ValidationErrors(Vec<ValidationError>),
|
|
||||||
/// Something already exists. That something should be {0}
|
|
||||||
/// Example: "User" "Credential"
|
|
||||||
#[error("Error {0} Already exists.")]
|
|
||||||
AlreadyExistsError(String),
|
|
||||||
/// Example: "User with id X"
|
|
||||||
#[error("{0} Not found.")]
|
|
||||||
NotFoundError(String),
|
|
||||||
/// Used to specify authentication error.
|
|
||||||
/// Example: Password incorrect for user
|
|
||||||
#[error("Credential supplied is incorrect. {0}")]
|
|
||||||
IncorrectCredentialError(String),
|
|
||||||
#[error("Too many credentials supplied, maximum is 3.")]
|
|
||||||
TooManyCredentialsError,
|
|
||||||
/// Used for anything else.
|
|
||||||
#[error("Unexpected Error: {0}")]
|
|
||||||
UnexpectedError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Any string validation error such as Phone number validation or email, etc...
|
|
||||||
/// Reason should be a Key for internationalization
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
#[error("Error validating `{what}`. Reason: {reason}")]
|
|
||||||
pub struct ValidationError {
|
|
||||||
pub what: String,
|
|
||||||
pub reason: String,
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use err::{Error, trace};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
encode::IsNull,
|
encode::IsNull,
|
||||||
error::BoxDynError,
|
error::BoxDynError,
|
||||||
@ -8,8 +9,6 @@ use sqlx::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::domain::credential::CredentialType;
|
use crate::domain::credential::CredentialType;
|
||||||
use crate::domain::error::Error;
|
|
||||||
use crate::domain::error::Error::FromStrError;
|
|
||||||
|
|
||||||
impl FromStr for CredentialType {
|
impl FromStr for CredentialType {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
@ -19,7 +18,7 @@ impl FromStr for CredentialType {
|
|||||||
"PhoneNumber" => Ok(Self::PhoneNumber),
|
"PhoneNumber" => Ok(Self::PhoneNumber),
|
||||||
"Email" => Ok(Self::Email),
|
"Email" => Ok(Self::Email),
|
||||||
"Username" => Ok(Self::Username),
|
"Username" => Ok(Self::Username),
|
||||||
_ => Err(FromStrError),
|
_ => Err(Error::new(trace!()).error_type(err::ErrorType::Parser).message(s)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
pub mod credential;
|
pub mod credential;
|
||||||
pub mod error;
|
|
||||||
pub mod impls;
|
pub mod impls;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
@ -7,17 +7,13 @@ use crate::domain::user::User;
|
|||||||
use crate::dto::token::{AuthenticateUserDto, RefreshAuthTokenForUserDto};
|
use crate::dto::token::{AuthenticateUserDto, RefreshAuthTokenForUserDto};
|
||||||
use crate::dto::users::{UserLoginPayload, UserRegisterPayload, UserResetPasswordPayload};
|
use crate::dto::users::{UserLoginPayload, UserRegisterPayload, UserResetPasswordPayload};
|
||||||
|
|
||||||
use crate::domain::error::Error::{
|
|
||||||
AlreadyExistsError, IncorrectCredentialError, NotFoundError, TooManyCredentialsError,
|
|
||||||
UnexpectedError,
|
|
||||||
};
|
|
||||||
use crate::domain::error::{Error, ValidationError};
|
|
||||||
use crate::resources::expirations::AUTH_TOKEN_EXPIRATION_TIME_MILLIS;
|
use crate::resources::expirations::AUTH_TOKEN_EXPIRATION_TIME_MILLIS;
|
||||||
use crate::utils::hasher::{
|
use crate::utils::hasher::{
|
||||||
generate_multiple_random_token_with_rng, hash_password, hash_password_with_existing_salt,
|
generate_multiple_random_token_with_rng, hash_password, hash_password_with_existing_salt,
|
||||||
};
|
};
|
||||||
use crate::validation::user_validator::validate_user_for_creation;
|
use crate::validation::user_validator::validate_user_for_creation;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use err::{trace, Error, ErrorType, ServiceError, ValidationError, x_u_res_db_or_res, u_res_or_res};
|
||||||
use log::error;
|
use log::error;
|
||||||
use sqlx::{PgConnection, Postgres, Transaction};
|
use sqlx::{PgConnection, Postgres, Transaction};
|
||||||
|
|
||||||
@ -30,13 +26,15 @@ pub async fn register_user<'a>(
|
|||||||
validate_user_for_creation(&user, &mut validation_errors);
|
validate_user_for_creation(&user, &mut validation_errors);
|
||||||
// Find if user exists
|
// Find if user exists
|
||||||
if user.credentials.len() > 3 {
|
if user.credentials.len() > 3 {
|
||||||
return Err(TooManyCredentialsError);
|
return Err(Error::new(trace!()));
|
||||||
}
|
}
|
||||||
for credential_dto in user.credentials.iter() {
|
for credential_dto in user.credentials.iter() {
|
||||||
match get_credential(transaction, credential_dto.credential.clone()).await? {
|
match x_u_res_db_or_res!(get_credential(transaction, credential_dto.credential.clone()).await) {
|
||||||
None => {}
|
None => {}
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
return Err(AlreadyExistsError(String::from("Credential")));
|
return Err(Error::new(trace!()).error_type(ErrorType::Service(
|
||||||
|
ServiceError::AlreadyExistsError(String::from("Credential")),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -52,11 +50,11 @@ pub async fn register_user<'a>(
|
|||||||
last_updated: now,
|
last_updated: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
let persisted_user = insert_user(transaction, user_to_insert).await?;
|
let persisted_user = x_u_res_db_or_res!(insert_user(transaction, user_to_insert).await);
|
||||||
|
|
||||||
// Insert Credentials
|
// Insert Credentials
|
||||||
for credential in user.credentials {
|
for credential in user.credentials {
|
||||||
insert_credential(transaction, credential, &persisted_user.id).await?;
|
x_u_res_db_or_res!(insert_credential(transaction, credential, &persisted_user.id).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
create_token_for_user(transaction, persisted_user.id).await
|
create_token_for_user(transaction, persisted_user.id).await
|
||||||
@ -65,23 +63,23 @@ pub async fn authenticate_user<'a>(
|
|||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
user: AuthenticateUserDto,
|
user: AuthenticateUserDto,
|
||||||
) -> Result<User, Error> {
|
) -> Result<User, Error> {
|
||||||
let persisted_user = match get_user_with_id(conn, &user.id).await? {
|
let persisted_user = match x_u_res_db_or_res!(get_user_with_id(conn, &user.id).await) {
|
||||||
None => {
|
None => {
|
||||||
return Err(NotFoundError(format!("User with id: {}", user.id)));
|
return Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::NotFoundError(format!("User with id: {}", user.id)))));
|
||||||
}
|
}
|
||||||
Some(persisted_user) => persisted_user,
|
Some(persisted_user) => persisted_user,
|
||||||
};
|
};
|
||||||
|
|
||||||
match validate_user_token(conn, &user.id, user.auth_token.clone()).await? {
|
match x_u_res_db_or_res!(validate_user_token(conn, &user.id, user.auth_token.clone()).await) {
|
||||||
None => Err(NotFoundError(format!("Auth Token {}", user.auth_token))),
|
None => return Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::NotFoundError(format!("Auth Token {}", user.auth_token))))),
|
||||||
Some(persisted_token) => {
|
Some(persisted_token) => {
|
||||||
// Check if persisted_token expired
|
// Check if persisted_token expired
|
||||||
if Utc::now().timestamp_millis() - persisted_token.last_updated.timestamp_millis()
|
if Utc::now().timestamp_millis() - persisted_token.last_updated.timestamp_millis()
|
||||||
> AUTH_TOKEN_EXPIRATION_TIME_MILLIS
|
> AUTH_TOKEN_EXPIRATION_TIME_MILLIS
|
||||||
{
|
{
|
||||||
Err(IncorrectCredentialError(String::from(
|
Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::IncorrectCredentialError(String::from(
|
||||||
"Auth Token Expired. Use refresh token to get a new one.",
|
"Auth Token Expired. Use refresh token to get a new one.",
|
||||||
)))
|
)))))
|
||||||
} else {
|
} else {
|
||||||
// Not expired
|
// Not expired
|
||||||
Ok(persisted_user)
|
Ok(persisted_user)
|
||||||
@ -95,9 +93,9 @@ pub async fn refresh_auth_token<'a>(
|
|||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
user: RefreshAuthTokenForUserDto,
|
user: RefreshAuthTokenForUserDto,
|
||||||
) -> Result<Token, Error> {
|
) -> Result<Token, Error> {
|
||||||
let _persisted_user = match get_user_with_id(conn, &user.id).await? {
|
let _persisted_user = match x_u_res_db_or_res!(get_user_with_id(conn, &user.id).await) {
|
||||||
None => {
|
None => {
|
||||||
return Err(NotFoundError(format!("User with id: {}", user.id)));
|
return Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::NotFoundError(format!("User with id: {}", user.id)))));
|
||||||
}
|
}
|
||||||
Some(persisted_user) => persisted_user,
|
Some(persisted_user) => persisted_user,
|
||||||
};
|
};
|
||||||
@ -106,9 +104,9 @@ pub async fn refresh_auth_token<'a>(
|
|||||||
|
|
||||||
if tokens.len() > 0 {
|
if tokens.len() > 0 {
|
||||||
let new_auth_token = tokens.remove(0);
|
let new_auth_token = tokens.remove(0);
|
||||||
Ok(update_token(conn, user.refresh_token, new_auth_token).await?)
|
Ok(x_u_res_db_or_res!(update_token(conn, user.refresh_token, new_auth_token).await))
|
||||||
} else {
|
} else {
|
||||||
Err(UnexpectedError(String::from("No tokens were created.")))
|
Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::UnexpectedError(String::from("No tokens were created.")))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,15 +115,15 @@ pub async fn reset_password(
|
|||||||
conn: &mut PgConnection,
|
conn: &mut PgConnection,
|
||||||
user: UserResetPasswordPayload,
|
user: UserResetPasswordPayload,
|
||||||
) -> Result<User, Error> {
|
) -> Result<User, Error> {
|
||||||
let password_matches = validate_user_password(conn, &user.id, user.password).await?;
|
let password_matches = u_res_or_res!(validate_user_password(conn, &user.id, user.password).await);
|
||||||
|
|
||||||
if let Some(persisted_user) = password_matches {
|
if let Some(persisted_user) = password_matches {
|
||||||
// Change pass
|
// Change pass
|
||||||
Ok(change_password(conn, persisted_user, &user.new_password).await?)
|
Ok(u_res_or_res!(change_password(conn, persisted_user, &user.new_password).await))
|
||||||
} else {
|
} else {
|
||||||
Err(IncorrectCredentialError(String::from(
|
Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::IncorrectCredentialError(String::from(
|
||||||
"Password incorrect.",
|
"Password incorrect.",
|
||||||
)))
|
)))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,14 +134,14 @@ pub async fn force_reset_password<'a>(
|
|||||||
user_id: &i32,
|
user_id: &i32,
|
||||||
new_password: String,
|
new_password: String,
|
||||||
) -> Result<User, Error> {
|
) -> Result<User, Error> {
|
||||||
let persisted_user = match get_user_with_id(conn, user_id).await? {
|
let persisted_user = match x_u_res_db_or_res!(get_user_with_id(conn, user_id).await) {
|
||||||
None => {
|
None => {
|
||||||
error!("Serious error. User doesn't exist but credentials pointing to the user do.");
|
error!("Serious error. User doesn't exist but credentials pointing to the user do.");
|
||||||
return Err(Error::NotFoundError(format!("User with id: {user_id}")));
|
return Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::NotFoundError(format!("User with id: {user_id}")))));
|
||||||
}
|
}
|
||||||
Some(persisted_user) => persisted_user,
|
Some(persisted_user) => persisted_user,
|
||||||
};
|
};
|
||||||
Ok(change_password(conn, persisted_user, &new_password).await?)
|
Ok(u_res_or_res!(change_password(conn, persisted_user, &new_password).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -151,19 +149,19 @@ pub async fn password_login<'a>(
|
|||||||
conn: &mut Transaction<'a, Postgres>,
|
conn: &mut Transaction<'a, Postgres>,
|
||||||
user: UserLoginPayload,
|
user: UserLoginPayload,
|
||||||
) -> Result<Token, Error> {
|
) -> Result<Token, Error> {
|
||||||
let persisted_user_credential = match get_credential(conn, user.credential.clone()).await? {
|
let persisted_user_credential = match x_u_res_db_or_res!(get_credential(conn, user.credential.clone()).await) {
|
||||||
None => return Err(NotFoundError(format!("Credential {}", user.credential))),
|
None => return Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::NotFoundError(format!("Credential {}", user.credential))))),
|
||||||
Some(persisted_credential) => persisted_credential,
|
Some(persisted_credential) => persisted_credential,
|
||||||
};
|
};
|
||||||
let persisted_user_opt =
|
let persisted_user_opt =
|
||||||
validate_user_password(conn, &persisted_user_credential.user_id, user.password).await?;
|
u_res_or_res!(validate_user_password(conn, &persisted_user_credential.user_id, user.password).await);
|
||||||
if let Some(_) = persisted_user_opt {
|
if let Some(_) = persisted_user_opt {
|
||||||
Ok(create_token_for_user(conn, persisted_user_credential.user_id).await?)
|
Ok(u_res_or_res!(create_token_for_user(conn, persisted_user_credential.user_id).await))
|
||||||
} else {
|
} else {
|
||||||
Err(NotFoundError(format!(
|
Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::NotFoundError(format!(
|
||||||
"User with id: {}",
|
"User with id: {}",
|
||||||
persisted_user_credential.user_id
|
persisted_user_credential.user_id
|
||||||
)))
|
)))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,8 +170,8 @@ pub async fn get_user_credentials<'a>(
|
|||||||
transaction: &mut Transaction<'a, Postgres>,
|
transaction: &mut Transaction<'a, Postgres>,
|
||||||
user: AuthenticateUserDto,
|
user: AuthenticateUserDto,
|
||||||
) -> Result<Vec<Credential>, Error> {
|
) -> Result<Vec<Credential>, Error> {
|
||||||
let persisted_user = authenticate_user(transaction, user).await?;
|
let persisted_user = u_res_or_res!(authenticate_user(transaction, user).await);
|
||||||
Ok(fetch_user_credentials(transaction, &persisted_user.id).await?)
|
Ok(x_u_res_db_or_res!(fetch_user_credentials(transaction, &persisted_user.id).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_token_for_user<'a>(
|
async fn create_token_for_user<'a>(
|
||||||
@ -188,18 +186,18 @@ async fn create_token_for_user<'a>(
|
|||||||
auth_token: match tokens.get(0) {
|
auth_token: match tokens.get(0) {
|
||||||
None => {
|
None => {
|
||||||
error!("Tokens were not created.");
|
error!("Tokens were not created.");
|
||||||
return Err(Error::UnexpectedError(String::from(
|
return Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::UnexpectedError(String::from(
|
||||||
"Tokens were not created.",
|
"Tokens were not created.",
|
||||||
)));
|
)))));
|
||||||
}
|
}
|
||||||
Some(token) => token.clone(),
|
Some(token) => token.clone(),
|
||||||
},
|
},
|
||||||
refresh_token: match tokens.get(1) {
|
refresh_token: match tokens.get(1) {
|
||||||
None => {
|
None => {
|
||||||
error!("Tokens were not created.");
|
error!("Tokens were not created.");
|
||||||
return Err(Error::UnexpectedError(String::from(
|
return Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::UnexpectedError(String::from(
|
||||||
"Tokens were not created.",
|
"Tokens were not created.",
|
||||||
)));
|
)))));
|
||||||
}
|
}
|
||||||
Some(token) => token.clone(),
|
Some(token) => token.clone(),
|
||||||
},
|
},
|
||||||
@ -208,7 +206,7 @@ async fn create_token_for_user<'a>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Insert token in DB
|
// Insert token in DB
|
||||||
Ok(insert_token(transaction, token_to_insert).await?)
|
Ok(x_u_res_db_or_res!(insert_token(transaction, token_to_insert).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn validate_user_password<'a>(
|
async fn validate_user_password<'a>(
|
||||||
@ -216,10 +214,10 @@ async fn validate_user_password<'a>(
|
|||||||
user_id: &i32,
|
user_id: &i32,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<Option<User>, Error> {
|
) -> Result<Option<User>, Error> {
|
||||||
let persisted_user = match get_user_with_id(conn, user_id).await? {
|
let persisted_user = match x_u_res_db_or_res!(get_user_with_id(conn, user_id).await) {
|
||||||
None => {
|
None => {
|
||||||
error!("Serious error. User doesn't exist but credentials pointing to the user do.");
|
error!("Serious error. User doesn't exist but credentials pointing to the user do.");
|
||||||
return Err(Error::NotFoundError(format!("User with id: {user_id}")));
|
return Err(Error::new(trace!()).error_type(ErrorType::Service(ServiceError::NotFoundError(format!("User with id: {user_id}")))));
|
||||||
}
|
}
|
||||||
Some(persisted_user) => persisted_user,
|
Some(persisted_user) => persisted_user,
|
||||||
};
|
};
|
||||||
@ -240,5 +238,5 @@ async fn change_password<'a>(
|
|||||||
let hash_result = hash_password(&new_password);
|
let hash_result = hash_password(&new_password);
|
||||||
persisted_user.password = hash_result.hash;
|
persisted_user.password = hash_result.hash;
|
||||||
persisted_user.salt = hash_result.salt;
|
persisted_user.salt = hash_result.salt;
|
||||||
Ok(update_user(conn, persisted_user).await?)
|
Ok(x_u_res_db_or_res!(update_user(conn, persisted_user).await))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
use err::ValidationError;
|
||||||
|
|
||||||
use crate::domain::credential::CredentialType;
|
use crate::domain::credential::CredentialType;
|
||||||
use crate::domain::error::ValidationError;
|
|
||||||
use crate::dto::users::{UserLoginPayload, UserRegisterPayload};
|
use crate::dto::users::{UserLoginPayload, UserRegisterPayload};
|
||||||
use crate::resources::error_messages::ERROR_INVALID_USERNAME;
|
use crate::resources::error_messages::ERROR_INVALID_USERNAME;
|
||||||
use crate::resources::{
|
use crate::resources::{
|
||||||
|
Loading…
Reference in New Issue
Block a user