Shulkers of fixes (#327)

* Shulkers of fixes

* Fix validation message

* Update deps

* Bump docker image version
This commit is contained in:
Geometrically
2022-03-27 19:12:42 -07:00
committed by GitHub
parent 7415b07586
commit d1c0c9739d
42 changed files with 683 additions and 700 deletions

View File

@@ -28,7 +28,7 @@ macro_rules! generate_ids {
retry_count += 1;
if retry_count > ID_RETRY_COUNT {
return Err(DatabaseError::RandomIdError);
return Err(DatabaseError::RandomId);
}
}

View File

@@ -24,16 +24,16 @@ pub use version_item::VersionFile;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Error while interacting with the database: {0}")]
DatabaseError(#[from] sqlx::error::Error),
Database(#[from] sqlx::error::Error),
#[error("Error while trying to generate random ID")]
RandomIdError,
RandomId,
#[error(
"Invalid identifier: Category/version names must contain only ASCII \
alphanumeric characters or '_-'."
)]
InvalidIdentifier(String),
#[error("Invalid permissions bitflag!")]
BitflagError,
Bitflag,
#[error("A database request failed")]
Other(String),
}

View File

@@ -124,7 +124,7 @@ impl TeamMember {
accepted: m.accepted,
})))
} else {
Ok(Some(Err(super::DatabaseError::BitflagError)))
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
} else {
Ok(None)
@@ -186,7 +186,7 @@ impl TeamMember {
},
})))
} else {
Ok(Some(Err(super::DatabaseError::BitflagError)))
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
} else {
Ok(None)
@@ -234,7 +234,7 @@ impl TeamMember {
accepted: m.accepted,
})))
} else {
Ok(Some(Err(super::DatabaseError::BitflagError)))
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
} else {
Ok(None)
@@ -282,7 +282,7 @@ impl TeamMember {
accepted: m.accepted,
})))
} else {
Ok(Some(Err(super::DatabaseError::BitflagError)))
Ok(Some(Err(super::DatabaseError::Bitflag)))
}
} else {
Ok(None)
@@ -326,7 +326,7 @@ impl TeamMember {
user_id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or(super::DatabaseError::BitflagError)?,
.ok_or(super::DatabaseError::Bitflag)?,
accepted: m.accepted,
}))
} else {
@@ -362,7 +362,7 @@ impl TeamMember {
user_id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or(super::DatabaseError::BitflagError)?,
.ok_or(super::DatabaseError::Bitflag)?,
accepted: m.accepted,
}))
} else {
@@ -510,7 +510,7 @@ impl TeamMember {
user_id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or(super::DatabaseError::BitflagError)?,
.ok_or(super::DatabaseError::Bitflag)?,
accepted: m.accepted,
}))
} else {
@@ -546,7 +546,7 @@ impl TeamMember {
user_id,
role: m.role,
permissions: Permissions::from_bits(m.permissions as u64)
.ok_or(super::DatabaseError::BitflagError)?,
.ok_or(super::DatabaseError::Bitflag)?,
accepted: m.accepted,
}))
} else {

View File

@@ -460,7 +460,7 @@ impl Version {
sqlx::query!(
"
DELETE FROM dependencies WHERE dependent_id = $1 AND dependency_id = $1
DELETE FROM dependencies WHERE dependent_id = $1
",
id as VersionId,
)

View File

@@ -31,8 +31,6 @@ struct Config {
#[options(no_short, help = "Skip indexing on startup")]
skip_first_index: bool,
#[options(no_short, help = "Reset the settings of the indices")]
reconfigure_indices: bool,
#[options(no_short, help = "Reset the documents in the indices")]
reset_indices: bool,
@@ -79,12 +77,6 @@ async fn main() -> std::io::Result<()> {
.await
.unwrap();
return Ok(());
} else if config.reconfigure_indices {
info!("Reconfiguring indices");
search::indexing::reconfigure_indices(&search_config)
.await
.unwrap();
return Ok(());
}
// Allow manually skipping the initial indexing for quicker iteration
@@ -252,18 +244,18 @@ async fn main() -> std::io::Result<()> {
if let Some(header) =
req.headers().get("CF-Connecting-IP")
{
header.to_str().map_err(|_| {
ARError::IdentificationError
})?
header
.to_str()
.map_err(|_| ARError::Identification)?
} else {
connection_info
.peer_addr()
.ok_or(ARError::IdentificationError)?
.ok_or(ARError::Identification)?
}
} else {
connection_info
.peer_addr()
.ok_or(ARError::IdentificationError)?
.ok_or(ARError::Identification)?
},
);

View File

@@ -475,13 +475,14 @@ pub struct Loader(pub String);
#[derive(Serialize, Deserialize)]
pub struct SearchRequest {
pub query: Option<String>,
/// Must match a json 2 deep array of strings `[["categories:misc"]]`
// TODO: We may want to have a better representation of this, so that
// we are less likely to break backwards compatibility
pub facets: Option<String>,
pub filters: Option<String>,
pub version: Option<String>,
pub offset: Option<String>,
pub index: Option<String>,
pub limit: Option<String>,
pub new_filters: Option<String>,
// Deprecated values below. WILL BE REMOVED V3!
pub facets: Option<String>,
pub filters: Option<String>,
pub version: Option<String>,
}

View File

@@ -1,4 +1,5 @@
//! Errors that can occur during middleware processing stage
use crate::models::error::ApiError;
use actix_web::ResponseError;
use log::*;
use thiserror::Error;
@@ -11,14 +12,14 @@ use thiserror::Error;
pub enum ARError {
/// Read/Write error on store
#[error("read/write operatiion failed: {0}")]
ReadWriteError(String),
ReadWrite(String),
/// Identifier error
#[error("client identification failed")]
IdentificationError,
Identification,
/// Limited Error
#[error("You are being ratelimited. Please wait {reset} seconds. {remaining}/{max_requests} remaining.")]
LimitedError {
Limited {
max_requests: usize,
remaining: usize,
reset: u64,
@@ -28,7 +29,7 @@ pub enum ARError {
impl ResponseError for ARError {
fn error_response(&self) -> actix_web::HttpResponse {
match self {
Self::LimitedError {
Self::Limited {
max_requests,
remaining,
reset,
@@ -44,10 +45,17 @@ impl ResponseError for ARError {
));
response
.insert_header(("x-ratelimit-reset", reset.to_string()));
response.body(self.to_string())
response.json(ApiError {
error: "ratelimit_error",
description: &self.to_string(),
})
}
_ => actix_web::HttpResponse::build(self.status_code())
.body(self.to_string()),
_ => actix_web::HttpResponse::build(self.status_code()).json(
ApiError {
error: "ratelimit_error",
description: &self.to_string(),
},
),
}
}
}

View File

@@ -107,13 +107,11 @@ impl Handler<ActorMessage> for MemoryStoreActor {
let new_val = val_mut.0;
ActorResponse::Update(Box::pin(future::ready(Ok(new_val))))
}
None => {
return ActorResponse::Update(Box::pin(future::ready(Err(
ARError::ReadWriteError(
"memory store: read failed!".to_string(),
),
))))
}
None => ActorResponse::Update(Box::pin(future::ready(Err(
ARError::ReadWrite(
"memory store: read failed!".to_string(),
),
)))),
},
ActorMessage::Get(key) => {
if self.inner.contains_key(&key) {
@@ -121,7 +119,7 @@ impl Handler<ActorMessage> for MemoryStoreActor {
Some(c) => c,
None => {
return ActorResponse::Get(Box::pin(future::ready(
Err(ARError::ReadWriteError(
Err(ARError::ReadWrite(
"memory store: read failed!".to_string(),
)),
)))
@@ -138,7 +136,7 @@ impl Handler<ActorMessage> for MemoryStoreActor {
Some(d) => d,
None => {
return ActorResponse::Expire(Box::pin(future::ready(
Err(ARError::ReadWriteError(
Err(ARError::ReadWrite(
"memory store: read failed!".to_string(),
)),
)))
@@ -156,7 +154,7 @@ impl Handler<ActorMessage> for MemoryStoreActor {
Some(c) => c,
None => {
return ActorResponse::Remove(Box::pin(future::ready(
Err(ARError::ReadWriteError(
Err(ARError::ReadWrite(
"memory store: remove failed!".to_string(),
)),
)))

View File

@@ -1,4 +1,3 @@
//! RateLimiter middleware for actix application
use crate::ratelimit::errors::ARError;
use crate::ratelimit::{ActorMessage, ActorResponse};
use actix::dev::*;
@@ -19,28 +18,9 @@ use std::{
time::Duration,
};
/// Type that implements the ratelimit middleware.
///
/// This accepts _interval_ which specifies the
/// window size, _max_requests_ which specifies the maximum number of requests in that window, and
/// _store_ which is essentially a data store used to store client access information. Entry is removed from
/// the store after _interval_.
///
/// # Example
/// ```rust
/// # use std::time::Duration;
/// use actix_ratelimit::{MemoryStore, MemoryStoreActor};
/// use actix_ratelimit::RateLimiter;
///
/// #[actix_rt::main]
/// async fn main() {
/// let store = MemoryStore::new();
/// let ratelimiter = RateLimiter::new(
/// MemoryStoreActor::from(store.clone()).start())
/// .with_interval(Duration::from_secs(60))
/// .with_max_requests(100);
/// }
/// ```
type RateLimiterIdentifier =
Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError> + 'static>>;
pub struct RateLimiter<T>
where
T: Handler<ActorMessage> + Send + Sync + 'static,
@@ -49,7 +29,7 @@ where
interval: Duration,
max_requests: usize,
store: Addr<T>,
identifier: Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError>>>,
identifier: RateLimiterIdentifier,
ignore_ips: Vec<String>,
}
@@ -62,9 +42,8 @@ where
pub fn new(store: Addr<T>) -> Self {
let identifier = |req: &ServiceRequest| {
let connection_info = req.connection_info();
let ip = connection_info
.peer_addr()
.ok_or(ARError::IdentificationError)?;
let ip =
connection_info.peer_addr().ok_or(ARError::Identification)?;
Ok(String::from(ip))
};
RateLimiter {
@@ -144,8 +123,7 @@ where
// Exists here for the sole purpose of knowing the max_requests and interval from RateLimiter
max_requests: usize,
interval: u64,
identifier:
Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError> + 'static>>,
identifier: RateLimiterIdentifier,
ignore_ips: Vec<String>,
}
@@ -187,7 +165,7 @@ where
let remaining: ActorResponse = store
.send(ActorMessage::Get(String::from(&identifier)))
.await
.map_err(|_| ARError::IdentificationError)?;
.map_err(|_| ARError::Identification)?;
match remaining {
ActorResponse::Get(opt) => {
let opt = opt.await?;
@@ -199,7 +177,7 @@ where
)))
.await
.map_err(|_| {
ARError::ReadWriteError(
ARError::ReadWrite(
"Setting timeout".to_string(),
)
})?;
@@ -209,7 +187,7 @@ where
};
if c == 0 {
info!("Limit exceeded for client: {}", &identifier);
Err(ARError::LimitedError {
Err(ARError::Limited {
max_requests,
remaining: c,
reset: reset.as_secs(),
@@ -224,7 +202,7 @@ where
})
.await
.map_err(|_| {
ARError::ReadWriteError(
ARError::ReadWrite(
"Decrementing ratelimit".to_string(),
)
})?;
@@ -270,7 +248,7 @@ where
})
.await
.map_err(|_| {
ARError::ReadWriteError(
ARError::ReadWrite(
"Creating store entry".to_string(),
)
})?;

View File

@@ -19,61 +19,51 @@ pub fn config(cfg: &mut ServiceConfig) {
#[derive(Error, Debug)]
pub enum AuthorizationError {
#[error("Environment Error")]
EnvError(#[from] dotenv::Error),
Env(#[from] dotenv::Error),
#[error("An unknown database error occured: {0}")]
SqlxDatabaseError(#[from] sqlx::Error),
SqlxDatabase(#[from] sqlx::Error),
#[error("Database Error: {0}")]
DatabaseError(#[from] crate::database::models::DatabaseError),
Database(#[from] crate::database::models::DatabaseError),
#[error("Error while parsing JSON: {0}")]
SerDeError(#[from] serde_json::Error),
SerDe(#[from] serde_json::Error),
#[error("Error while communicating to GitHub OAuth2")]
GithubError(#[from] reqwest::Error),
Github(#[from] reqwest::Error),
#[error("Invalid Authentication credentials")]
InvalidCredentialsError,
InvalidCredentials,
#[error("Authentication Error: {0}")]
AuthenticationError(#[from] crate::util::auth::AuthenticationError),
Authentication(#[from] crate::util::auth::AuthenticationError),
#[error("Error while decoding Base62")]
DecodingError(#[from] DecodingError),
Decoding(#[from] DecodingError),
}
impl actix_web::ResponseError for AuthorizationError {
fn status_code(&self) -> StatusCode {
match self {
AuthorizationError::EnvError(..) => {
AuthorizationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::SqlxDatabase(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthorizationError::SqlxDatabaseError(..) => {
AuthorizationError::Database(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthorizationError::DatabaseError(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthorizationError::SerDeError(..) => StatusCode::BAD_REQUEST,
AuthorizationError::GithubError(..) => {
StatusCode::FAILED_DEPENDENCY
}
AuthorizationError::InvalidCredentialsError => {
StatusCode::UNAUTHORIZED
}
AuthorizationError::DecodingError(..) => StatusCode::BAD_REQUEST,
AuthorizationError::AuthenticationError(..) => {
StatusCode::UNAUTHORIZED
}
AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST,
AuthorizationError::Github(..) => StatusCode::FAILED_DEPENDENCY,
AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED,
AuthorizationError::Decoding(..) => StatusCode::BAD_REQUEST,
AuthorizationError::Authentication(..) => StatusCode::UNAUTHORIZED,
}
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).json(ApiError {
error: match self {
AuthorizationError::EnvError(..) => "environment_error",
AuthorizationError::SqlxDatabaseError(..) => "database_error",
AuthorizationError::DatabaseError(..) => "database_error",
AuthorizationError::SerDeError(..) => "invalid_input",
AuthorizationError::GithubError(..) => "github_error",
AuthorizationError::InvalidCredentialsError => {
"invalid_credentials"
}
AuthorizationError::DecodingError(..) => "decoding_error",
AuthorizationError::AuthenticationError(..) => {
AuthorizationError::Env(..) => "environment_error",
AuthorizationError::SqlxDatabase(..) => "database_error",
AuthorizationError::Database(..) => "database_error",
AuthorizationError::SerDe(..) => "invalid_input",
AuthorizationError::Github(..) => "github_error",
AuthorizationError::InvalidCredentials => "invalid_credentials",
AuthorizationError::Decoding(..) => "decoding_error",
AuthorizationError::Authentication(..) => {
"authentication_error"
}
},
@@ -159,7 +149,7 @@ pub async fn auth_callback(
let duration = result.expires.signed_duration_since(now);
if duration.num_seconds() < 0 {
return Err(AuthorizationError::InvalidCredentialsError);
return Err(AuthorizationError::InvalidCredentials);
}
sqlx::query!(
@@ -256,6 +246,6 @@ pub async fn auth_callback(
.append_header(("Location", &*redirect_url))
.json(AuthorizationInit { url: redirect_url }))
} else {
Err(AuthorizationError::InvalidCredentialsError)
Err(AuthorizationError::InvalidCredentials)
}
}

View File

@@ -114,7 +114,7 @@ pub async fn maven_metadata(
Ok(HttpResponse::Ok()
.content_type("text/xml")
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?))
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?))
}
fn find_file<'a>(
@@ -211,9 +211,9 @@ pub async fn version_file(
name: project.inner.title,
description: project.inner.description,
};
return Ok(HttpResponse::Ok().content_type("text/xml").body(
yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?,
));
return Ok(HttpResponse::Ok()
.content_type("text/xml")
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?));
} else if let Some(selected_file) =
find_file(&project_id, &project, &version, &file)
{

View File

@@ -159,66 +159,66 @@ pub fn reports_config(cfg: &mut web::ServiceConfig) {
#[derive(thiserror::Error, Debug)]
pub enum ApiError {
#[error("Environment Error")]
EnvError(#[from] dotenv::Error),
Env(#[from] dotenv::Error),
#[error("Error while uploading file")]
FileHostingError(#[from] FileHostingError),
FileHosting(#[from] FileHostingError),
#[error("Database Error: {0}")]
DatabaseError(#[from] crate::database::models::DatabaseError),
Database(#[from] crate::database::models::DatabaseError),
#[error("Database Error: {0}")]
SqlxDatabaseError(#[from] sqlx::Error),
SqlxDatabase(#[from] sqlx::Error),
#[error("Internal server error: {0}")]
XmlError(String),
Xml(String),
#[error("Deserialization error: {0}")]
JsonError(#[from] serde_json::Error),
Json(#[from] serde_json::Error),
#[error("Authentication Error: {0}")]
AuthenticationError(#[from] crate::util::auth::AuthenticationError),
Authentication(#[from] crate::util::auth::AuthenticationError),
#[error("Authentication Error: {0}")]
CustomAuthenticationError(String),
CustomAuthentication(String),
#[error("Invalid Input: {0}")]
InvalidInputError(String),
InvalidInput(String),
#[error("Error while validating input: {0}")]
ValidationError(String),
Validation(String),
#[error("Search Error: {0}")]
SearchError(#[from] meilisearch_sdk::errors::Error),
Search(#[from] meilisearch_sdk::errors::Error),
#[error("Indexing Error: {0}")]
IndexingError(#[from] crate::search::indexing::IndexingError),
Indexing(#[from] crate::search::indexing::IndexingError),
}
impl actix_web::ResponseError for ApiError {
fn status_code(&self) -> actix_web::http::StatusCode {
match self {
ApiError::EnvError(..) => {
ApiError::Env(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::DatabaseError(..) => {
ApiError::Database(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::SqlxDatabaseError(..) => {
ApiError::SqlxDatabase(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::AuthenticationError(..) => {
ApiError::Authentication(..) => {
actix_web::http::StatusCode::UNAUTHORIZED
}
ApiError::CustomAuthenticationError(..) => {
ApiError::CustomAuthentication(..) => {
actix_web::http::StatusCode::UNAUTHORIZED
}
ApiError::XmlError(..) => {
ApiError::Xml(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::JsonError(..) => actix_web::http::StatusCode::BAD_REQUEST,
ApiError::SearchError(..) => {
ApiError::Json(..) => actix_web::http::StatusCode::BAD_REQUEST,
ApiError::Search(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::IndexingError(..) => {
ApiError::Indexing(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::FileHostingError(..) => {
ApiError::FileHosting(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::InvalidInputError(..) => {
ApiError::InvalidInput(..) => {
actix_web::http::StatusCode::BAD_REQUEST
}
ApiError::ValidationError(..) => {
ApiError::Validation(..) => {
actix_web::http::StatusCode::BAD_REQUEST
}
}
@@ -228,18 +228,18 @@ impl actix_web::ResponseError for ApiError {
actix_web::HttpResponse::build(self.status_code()).json(
crate::models::error::ApiError {
error: match self {
ApiError::EnvError(..) => "environment_error",
ApiError::SqlxDatabaseError(..) => "database_error",
ApiError::DatabaseError(..) => "database_error",
ApiError::AuthenticationError(..) => "unauthorized",
ApiError::CustomAuthenticationError(..) => "unauthorized",
ApiError::XmlError(..) => "xml_error",
ApiError::JsonError(..) => "json_error",
ApiError::SearchError(..) => "search_error",
ApiError::IndexingError(..) => "indexing_error",
ApiError::FileHostingError(..) => "file_hosting_error",
ApiError::InvalidInputError(..) => "invalid_input",
ApiError::ValidationError(..) => "invalid_input",
ApiError::Env(..) => "environment_error",
ApiError::SqlxDatabase(..) => "database_error",
ApiError::Database(..) => "database_error",
ApiError::Authentication(..) => "unauthorized",
ApiError::CustomAuthentication(..) => "unauthorized",
ApiError::Xml(..) => "xml_error",
ApiError::Json(..) => "json_error",
ApiError::Search(..) => "search_error",
ApiError::Indexing(..) => "indexing_error",
ApiError::FileHosting(..) => "file_hosting_error",
ApiError::InvalidInput(..) => "invalid_input",
ApiError::Validation(..) => "invalid_input",
},
description: &self.to_string(),
},

View File

@@ -105,7 +105,7 @@ pub async fn notification_delete(
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::CustomAuthenticationError(
Err(ApiError::CustomAuthentication(
"You are not authorized to delete this notification!"
.to_string(),
))

View File

@@ -295,7 +295,7 @@ Get logged in user
pub async fn project_create_inner(
req: HttpRequest,
mut payload: Multipart,
mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
file_host: &dyn FileHost,
uploaded_files: &mut Vec<UploadedFile>,
) -> Result<HttpResponse, CreateError> {
@@ -535,7 +535,7 @@ pub async fn project_create_inner(
all_game_versions.clone(),
version_data.primary_file.is_some(),
version_data.primary_file.as_deref() == Some(name),
&mut transaction,
transaction,
)
.await?;
}

View File

@@ -102,9 +102,10 @@ pub async fn dependency_list(
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result =
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool)
.await?;
let result = database::models::Project::get_full_from_slug_or_project_id(
&string, &**pool,
)
.await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -152,7 +153,7 @@ pub async fn dependency_list(
database::Project::get_many_full(
dependencies
.iter()
.map(|x| if x.0.is_none() {
.filter_map(|x| if x.0.is_none() {
if let Some(mod_dependency_id) = x.2 {
Some(mod_dependency_id)
} else {
@@ -161,12 +162,11 @@ pub async fn dependency_list(
} else {
x.1
})
.flatten()
.collect(),
&**pool,
),
database::Version::get_many_full(
dependencies.iter().map(|x| x.0).flatten().collect(),
dependencies.iter().filter_map(|x| x.0).collect(),
&**pool,
)
);
@@ -282,7 +282,7 @@ pub async fn project_edit(
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_project.validate().map_err(|err| {
ApiError::ValidationError(validation_errors_to_string(err, None))
ApiError::Validation(validation_errors_to_string(err, None))
})?;
let string = info.into_inner().0;
@@ -315,7 +315,7 @@ pub async fn project_edit(
if let Some(title) = &new_project.title {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the title of this project!"
.to_string(),
));
@@ -336,7 +336,7 @@ pub async fn project_edit(
if let Some(description) = &new_project.description {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the description of this project!"
.to_string(),
));
@@ -357,7 +357,7 @@ pub async fn project_edit(
if let Some(status) = &new_project.status {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the status of this project!"
.to_string(),
));
@@ -367,7 +367,7 @@ pub async fn project_edit(
|| status == &ProjectStatus::Approved)
&& !user.role.is_mod()
{
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to set this status"
.to_string(),
));
@@ -375,7 +375,7 @@ pub async fn project_edit(
if status == &ProjectStatus::Processing {
if project_item.versions.is_empty() {
return Err(ApiError::InvalidInputError(String::from(
return Err(ApiError::InvalidInput(String::from(
"Project submitted for review with no initial versions",
)));
}
@@ -420,7 +420,7 @@ pub async fn project_edit(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"No database entry for status provided.".to_string(),
)
})?;
@@ -457,7 +457,7 @@ pub async fn project_edit(
if let Some(categories) = &new_project.categories {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the categories of this project!"
.to_string(),
));
@@ -481,7 +481,7 @@ pub async fn project_edit(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(format!(
ApiError::InvalidInput(format!(
"Category {} does not exist.",
category.clone()
))
@@ -502,7 +502,7 @@ pub async fn project_edit(
if let Some(issues_url) = &new_project.issues_url {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the issues URL of this project!"
.to_string(),
));
@@ -523,7 +523,7 @@ pub async fn project_edit(
if let Some(source_url) = &new_project.source_url {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the source URL of this project!"
.to_string(),
));
@@ -544,7 +544,7 @@ pub async fn project_edit(
if let Some(wiki_url) = &new_project.wiki_url {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the wiki URL of this project!"
.to_string(),
));
@@ -565,7 +565,7 @@ pub async fn project_edit(
if let Some(license_url) = &new_project.license_url {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the license URL of this project!"
.to_string(),
));
@@ -586,7 +586,7 @@ pub async fn project_edit(
if let Some(discord_url) = &new_project.discord_url {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the discord URL of this project!"
.to_string(),
));
@@ -607,7 +607,7 @@ pub async fn project_edit(
if let Some(slug) = &new_project.slug {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the slug of this project!"
.to_string(),
));
@@ -629,7 +629,7 @@ pub async fn project_edit(
.await?;
if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"Slug collides with other project's id!"
.to_string(),
));
@@ -652,7 +652,7 @@ pub async fn project_edit(
if let Some(new_side) = &new_project.client_side {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the side type of this mod!"
.to_string(),
));
@@ -680,7 +680,7 @@ pub async fn project_edit(
if let Some(new_side) = &new_project.server_side {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the side type of this project!"
.to_string(),
));
@@ -708,7 +708,7 @@ pub async fn project_edit(
if let Some(license) = &new_project.license_id {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the license of this project!"
.to_string(),
));
@@ -736,7 +736,7 @@ pub async fn project_edit(
if let Some(donations) = &new_project.donation_urls {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the donation links of this project!"
.to_string(),
));
@@ -760,7 +760,7 @@ pub async fn project_edit(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(format!(
ApiError::InvalidInput(format!(
"Platform {} does not exist.",
donation.id.clone()
))
@@ -784,7 +784,7 @@ pub async fn project_edit(
if !user.role.is_mod()
&& project_item.status != ProjectStatus::Approved
{
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the moderation message of this project!"
.to_string(),
));
@@ -809,7 +809,7 @@ pub async fn project_edit(
if !user.role.is_mod()
&& project_item.status != ProjectStatus::Approved
{
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the moderation message body of this project!"
.to_string(),
));
@@ -830,7 +830,7 @@ pub async fn project_edit(
if let Some(body) = &new_project.body {
if !perms.contains(Permissions::EDIT_BODY) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the body of this project!"
.to_string(),
));
@@ -852,7 +852,7 @@ pub async fn project_edit(
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::CustomAuthenticationError(
Err(ApiError::CustomAuthentication(
"You do not have permission to edit this project!".to_string(),
))
}
@@ -889,7 +889,7 @@ pub async fn project_icon_edit(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -901,15 +901,15 @@ pub async fn project_icon_edit(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's icon."
.to_string(),
));
@@ -958,7 +958,7 @@ pub async fn project_icon_edit(
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInputError(format!(
Err(ApiError::InvalidInput(format!(
"Invalid format for project icon: {}",
ext.ext
)))
@@ -981,7 +981,7 @@ pub async fn delete_project_icon(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -993,15 +993,15 @@ pub async fn delete_project_icon(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's icon."
.to_string(),
));
@@ -1057,7 +1057,7 @@ pub async fn add_gallery_item(
crate::util::ext::get_image_content_type(&*ext.ext)
{
item.validate().map_err(|err| {
ApiError::ValidationError(validation_errors_to_string(err, None))
ApiError::Validation(validation_errors_to_string(err, None))
})?;
let cdn_url = dotenv::var("CDN_URL")?;
@@ -1071,7 +1071,7 @@ pub async fn add_gallery_item(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -1083,15 +1083,15 @@ pub async fn add_gallery_item(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery."
.to_string(),
));
@@ -1143,7 +1143,7 @@ pub async fn add_gallery_item(
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInputError(format!(
Err(ApiError::InvalidInput(format!(
"Invalid format for gallery image: {}",
ext.ext
)))
@@ -1182,7 +1182,7 @@ pub async fn edit_gallery_item(
let string = info.into_inner().0;
item.validate().map_err(|err| {
ApiError::ValidationError(validation_errors_to_string(err, None))
ApiError::Validation(validation_errors_to_string(err, None))
})?;
let project_item = database::models::Project::get_from_slug_or_project_id(
@@ -1191,7 +1191,7 @@ pub async fn edit_gallery_item(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -1203,15 +1203,15 @@ pub async fn edit_gallery_item(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery."
.to_string(),
));
@@ -1229,7 +1229,7 @@ pub async fn edit_gallery_item(
.fetch_optional(&mut *transaction)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(format!(
ApiError::InvalidInput(format!(
"Gallery item at URL {} is not part of the project's gallery.",
item.url
))
@@ -1319,7 +1319,7 @@ pub async fn delete_gallery_item(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -1331,15 +1331,15 @@ pub async fn delete_gallery_item(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery."
.to_string(),
));
@@ -1357,7 +1357,7 @@ pub async fn delete_gallery_item(
.fetch_optional(&mut *transaction)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(format!(
ApiError::InvalidInput(format!(
"Gallery item at URL {} is not part of the project's gallery.",
item.url
))
@@ -1403,7 +1403,7 @@ pub async fn project_delete(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -1416,9 +1416,9 @@ pub async fn project_delete(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -1427,7 +1427,7 @@ pub async fn project_delete(
.permissions
.contains(Permissions::DELETE_PROJECT)
{
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to delete this project!".to_string(),
));
}
@@ -1463,7 +1463,7 @@ pub async fn project_follow(
database::models::Project::get_from_slug_or_project_id(string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -1512,7 +1512,7 @@ pub async fn project_follow(
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInputError(
Err(ApiError::InvalidInput(
"You are already following this project!".to_string(),
))
}
@@ -1531,7 +1531,7 @@ pub async fn project_unfollow(
database::models::Project::get_from_slug_or_project_id(string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
@@ -1580,7 +1580,7 @@ pub async fn project_unfollow(
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInputError(
Err(ApiError::InvalidInput(
"You are not following this project!".to_string(),
))
}

View File

@@ -31,7 +31,7 @@ pub async fn report_create(
let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await {
bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"Error while parsing request payload!".to_string(),
)
})?);
@@ -46,7 +46,7 @@ pub async fn report_create(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(format!(
ApiError::InvalidInput(format!(
"Invalid report type: {}",
new_report.report_type
))
@@ -91,7 +91,7 @@ pub async fn report_create(
)
}
ItemType::Unknown => {
return Err(ApiError::InvalidInputError(format!(
return Err(ApiError::InvalidInput(format!(
"Invalid report item type: {}",
new_report.item_type.as_str()
)))

View File

@@ -74,7 +74,7 @@ pub async fn category_create(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"Specified project type does not exist!".to_string(),
)
})?;

View File

@@ -38,7 +38,7 @@ pub async fn team_members_get_project(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?;
.map_err(ApiError::Database)?;
if team_member.is_some() {
let team_members: Vec<_> = members_data
@@ -80,7 +80,7 @@ pub async fn team_members_get(
let team_member =
TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
.await
.map_err(ApiError::DatabaseError)?;
.map_err(ApiError::Database)?;
if team_member.is_some() {
let team_members: Vec<_> = members_data
@@ -119,7 +119,7 @@ pub async fn join_team(
if let Some(member) = member {
if member.accepted {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"You are already a member of this team".to_string(),
));
}
@@ -138,7 +138,7 @@ pub async fn join_team(
transaction.commit().await?;
} else {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"There is no pending request from this team".to_string(),
));
}
@@ -175,26 +175,26 @@ pub async fn add_team_member(
TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
})?;
if !member.permissions.contains(Permissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to invite users to this team"
.to_string(),
));
}
if !member.permissions.contains(new_member.permissions) {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"The new member has permissions that you don't have".to_string(),
));
}
if new_member.role == crate::models::teams::OWNER_ROLE {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"The `Owner` role is restricted to one person".to_string(),
));
}
@@ -207,11 +207,11 @@ pub async fn add_team_member(
if let Some(req) = request {
if req.accepted {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"The user is already a member of that team".to_string(),
));
} else {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"There is already a pending member request for this user"
.to_string(),
));
@@ -221,9 +221,7 @@ pub async fn add_team_member(
crate::database::models::User::get(member.user_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
"An invalid User ID specified".to_string(),
)
ApiError::InvalidInput("An invalid User ID specified".to_string())
})?;
let new_id =
@@ -312,7 +310,7 @@ pub async fn edit_team_member(
TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
@@ -321,7 +319,7 @@ pub async fn edit_team_member(
TeamMember::get_from_user_id_pending(id, user_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
@@ -330,13 +328,13 @@ pub async fn edit_team_member(
let mut transaction = pool.begin().await?;
if &*edit_member_db.role == crate::models::teams::OWNER_ROLE {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"The owner of a team cannot be edited".to_string(),
));
}
if !member.permissions.contains(Permissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
));
@@ -344,7 +342,7 @@ pub async fn edit_team_member(
if let Some(new_permissions) = edit_member.permissions {
if !member.permissions.contains(new_permissions) {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"The new permissions have permissions that you don't have"
.to_string(),
));
@@ -352,7 +350,7 @@ pub async fn edit_team_member(
}
if edit_member.role.as_deref() == Some(crate::models::teams::OWNER_ROLE) {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"The `Owner` role is restricted to one person".to_string(),
));
}
@@ -394,7 +392,7 @@ pub async fn transfer_ownership(
)
.await?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
@@ -406,20 +404,20 @@ pub async fn transfer_ownership(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"The new owner specified does not exist".to_string(),
)
})?;
if member.role != crate::models::teams::OWNER_ROLE {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit the ownership of this team"
.to_string(),
));
}
if !new_member.accepted {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"You can only transfer ownership to members who are currently in your team".to_string(),
));
}
@@ -466,7 +464,7 @@ pub async fn remove_team_member(
TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
@@ -478,7 +476,7 @@ pub async fn remove_team_member(
if let Some(delete_member) = delete_member {
if delete_member.role == crate::models::teams::OWNER_ROLE {
// The owner cannot be removed from a team
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"The owner can't be removed from a team".to_string(),
));
}
@@ -492,7 +490,7 @@ pub async fn remove_team_member(
{
TeamMember::delete(id, user_id, &**pool).await?;
} else {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this team".to_string(),
));
}
@@ -505,7 +503,7 @@ pub async fn remove_team_member(
// permission can remove it.
TeamMember::delete(id, user_id, &**pool).await?;
} else {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel a team invite"
.to_string(),
));

View File

@@ -24,12 +24,12 @@ pub async fn forge_updates(
&id, &**pool,
)
.await?
.ok_or_else(|| ApiError::InvalidInputError(ERROR.to_string()))?;
.ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if !is_authorized(&project, &user_option, &pool).await? {
return Err(ApiError::InvalidInputError(ERROR.to_string()));
return Err(ApiError::InvalidInput(ERROR.to_string()));
}
let version_ids = database::models::Version::get_project_versions(

View File

@@ -166,7 +166,7 @@ pub async fn user_edit(
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_user.validate().map_err(|err| {
ApiError::ValidationError(validation_errors_to_string(err, None))
ApiError::Validation(validation_errors_to_string(err, None))
})?;
let id_option = crate::database::models::User::get_id_from_username_or_id(
@@ -201,7 +201,7 @@ pub async fn user_edit(
.execute(&mut *transaction)
.await?;
} else {
return Err(ApiError::InvalidInputError(format!(
return Err(ApiError::InvalidInput(format!(
"Username {} is taken!",
username
)));
@@ -252,7 +252,7 @@ pub async fn user_edit(
if let Some(role) = &new_user.role {
if !user.role.is_mod() {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the role of this user!"
.to_string(),
));
@@ -276,7 +276,7 @@ pub async fn user_edit(
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::CustomAuthenticationError(
Err(ApiError::CustomAuthentication(
"You do not have permission to edit this user!".to_string(),
))
}
@@ -313,7 +313,7 @@ pub async fn user_icon_edit(
if let Some(id) = id_option {
if user.id != id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this user's icon."
.to_string(),
));
@@ -374,7 +374,7 @@ pub async fn user_icon_edit(
Ok(HttpResponse::NotFound().body(""))
}
} else {
Err(ApiError::InvalidInputError(format!(
Err(ApiError::InvalidInput(format!(
"Invalid format for user icon: {}",
ext.ext
)))
@@ -407,24 +407,18 @@ pub async fn user_delete(
if let Some(id) = id_option {
if !user.role.is_mod() && user.id != id.into() {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have permission to delete this user!".to_string(),
));
}
let mut transaction = pool.begin().await?;
let result;
if &*removal_type.removal_type == "full" {
result = crate::database::models::User::remove_full(
id,
&mut transaction,
)
.await?;
let result = if &*removal_type.removal_type == "full" {
crate::database::models::User::remove_full(id, &mut transaction)
.await?
} else {
result =
crate::database::models::User::remove(id, &mut transaction)
.await?;
crate::database::models::User::remove(id, &mut transaction).await?
};
transaction.commit().await?;
@@ -454,7 +448,7 @@ pub async fn user_follows(
if let Some(id) = id_option {
if !user.role.is_mod() && user.id != id.into() {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have permission to see the projects this user follows!".to_string(),
));
}
@@ -504,7 +498,7 @@ pub async fn user_notifications(
if let Some(id) = id_option {
if !user.role.is_mod() && user.id != id.into() {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have permission to see the notifications of this user!".to_string(),
));
}

View File

@@ -64,7 +64,7 @@ pub async fn report_create(
let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await {
bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"Error while parsing request payload!".to_string(),
)
})?);
@@ -79,7 +79,7 @@ pub async fn report_create(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(format!(
ApiError::InvalidInput(format!(
"Invalid report type: {}",
new_report.report_type
))
@@ -124,7 +124,7 @@ pub async fn report_create(
)
}
ItemType::Unknown => {
return Err(ApiError::InvalidInputError(format!(
return Err(ApiError::InvalidInput(format!(
"Invalid report item type: {}",
new_report.item_type.as_str()
)))

View File

@@ -38,7 +38,7 @@ pub async fn category_create(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"Specified project type does not exist!".to_string(),
)
})?;

View File

@@ -42,7 +42,7 @@ pub async fn team_members_get(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?;
.map_err(ApiError::Database)?;
if team_member.is_some() {
let team_members: Vec<TeamMember> = members_data

View File

@@ -66,7 +66,7 @@ pub async fn user_follows(
if let Some(id) = id_option {
if !user.role.is_mod() && user.id != id.into() {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have permission to see the projects this user follows!".to_string(),
));
}

View File

@@ -269,7 +269,7 @@ pub async fn download_version(
)
.fetch_optional(&**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
.map_err(|e| ApiError::Database(e.into()))?;
if let Some(id) = result {
Ok(HttpResponse::TemporaryRedirect()
@@ -316,9 +316,9 @@ pub async fn delete_file(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
ApiError::CustomAuthentication(
"You don't have permission to delete this file!"
.to_string(),
)
@@ -328,7 +328,7 @@ pub async fn delete_file(
.permissions
.contains(Permissions::DELETE_VERSION)
{
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to delete this file!"
.to_string(),
));

View File

@@ -103,7 +103,7 @@ pub async fn version_create(
async fn version_create_inner(
req: HttpRequest,
mut payload: Multipart,
mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
file_host: &dyn FileHost,
uploaded_files: &mut Vec<UploadedFile>,
) -> Result<HttpResponse, CreateError> {
@@ -322,7 +322,7 @@ async fn version_create_inner(
all_game_versions.clone(),
version_data.primary_file.is_some(),
version_data.primary_file.as_deref() == Some(name),
&mut transaction,
transaction,
)
.await?;
}
@@ -486,7 +486,7 @@ async fn upload_file_to_version_inner(
req: HttpRequest,
mut payload: Multipart,
client: Data<PgPool>,
mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
file_host: &dyn FileHost,
uploaded_files: &mut Vec<UploadedFile>,
version_id: models::VersionId,
@@ -597,7 +597,7 @@ async fn upload_file_to_version_inner(
all_game_versions.clone(),
true,
false,
&mut transaction,
transaction,
)
.await?;
}

View File

@@ -135,9 +135,9 @@ pub async fn delete_file(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
ApiError::CustomAuthentication(
"You don't have permission to delete this file!"
.to_string(),
)
@@ -147,7 +147,7 @@ pub async fn delete_file(
.permissions
.contains(Permissions::DELETE_VERSION)
{
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You don't have permission to delete this file!"
.to_string(),
));
@@ -169,7 +169,7 @@ pub async fn delete_file(
.await?;
if files.len() < 2 {
return Err(ApiError::InvalidInputError(
return Err(ApiError::InvalidInput(
"Versions must have at least one file uploaded to them"
.to_string(),
));

View File

@@ -28,9 +28,10 @@ pub async fn version_list(
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result =
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool)
.await?;
let result = database::models::Project::get_full_from_slug_or_project_id(
&string, &**pool,
)
.await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -187,6 +188,7 @@ pub struct EditVersion {
pub loaders: Option<Vec<models::projects::Loader>>,
pub featured: Option<bool>,
pub primary_file: Option<(String, String)>,
pub downloads: Option<u32>,
}
#[patch("{id}")]
@@ -199,7 +201,7 @@ pub async fn version_edit(
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_version.validate().map_err(|err| {
ApiError::ValidationError(validation_errors_to_string(err, None))
ApiError::Validation(validation_errors_to_string(err, None))
})?;
let version_id = info.into_inner().0;
@@ -227,7 +229,7 @@ pub async fn version_edit(
if let Some(perms) = permissions {
if !perms.contains(Permissions::UPLOAD_VERSION) {
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit this version!"
.to_string(),
));
@@ -321,7 +323,7 @@ pub async fn version_edit(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"No database entry for game version provided."
.to_string(),
)
@@ -358,7 +360,7 @@ pub async fn version_edit(
)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"No database entry for loader provided."
.to_string(),
)
@@ -404,7 +406,7 @@ pub async fn version_edit(
.fetch_optional(&**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError(format!(
ApiError::InvalidInput(format!(
"Specified file with hash {} does not exist.",
primary_file.1.clone()
))
@@ -447,10 +449,38 @@ pub async fn version_edit(
.await?;
}
if let Some(downloads) = &new_version.downloads {
sqlx::query!(
"
UPDATE versions
SET downloads = $1
WHERE (id = $2)
",
*downloads as i32,
id as database::models::ids::VersionId,
)
.execute(&mut *transaction)
.await?;
let diff = *downloads - (version_item.downloads as u32);
sqlx::query!(
"
UPDATE mods
SET downloads = downloads + $1
WHERE (id = $2)
",
diff as i32,
version_item.project_id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::CustomAuthenticationError(
Err(ApiError::CustomAuthentication(
"You do not have permission to edit this version!".to_string(),
))
}
@@ -503,7 +533,7 @@ pub async fn version_count_patch(
.execute(pool.as_ref()),
)
.await
.map_err(ApiError::SqlxDatabaseError)?;
.map_err(ApiError::SqlxDatabase)?;
Ok(HttpResponse::Ok().body(""))
}
@@ -524,9 +554,9 @@ pub async fn version_delete(
&**pool,
)
.await
.map_err(ApiError::DatabaseError)?
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"You do not have permission to delete versions in this team".to_string(),
)
})?;
@@ -535,7 +565,7 @@ pub async fn version_delete(
.permissions
.contains(Permissions::DELETE_VERSION)
{
return Err(ApiError::CustomAuthenticationError(
return Err(ApiError::CustomAuthentication(
"You do not have permission to delete versions in this team"
.to_string(),
));

View File

@@ -8,23 +8,24 @@ use meilisearch_sdk::client::Client;
use meilisearch_sdk::indexes::Index;
use meilisearch_sdk::settings::Settings;
use sqlx::postgres::PgPool;
use std::collections::{HashMap, VecDeque};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum IndexingError {
#[error("Error while connecting to the MeiliSearch database")]
IndexDBError(#[from] meilisearch_sdk::errors::Error),
Indexing(#[from] meilisearch_sdk::errors::Error),
#[error("Error while serializing or deserializing JSON: {0}")]
SerdeError(#[from] serde_json::Error),
Serde(#[from] serde_json::Error),
#[error("Error while parsing a timestamp: {0}")]
ParseDateError(#[from] chrono::format::ParseError),
ParseDate(#[from] chrono::format::ParseError),
#[error("Database Error: {0}")]
SqlxError(#[from] sqlx::error::Error),
Sqlx(#[from] sqlx::error::Error),
#[error("Database Error: {0}")]
DatabaseError(#[from] crate::database::models::DatabaseError),
Database(#[from] crate::database::models::DatabaseError),
#[error("Environment Error")]
EnvError(#[from] dotenv::Error),
Env(#[from] dotenv::Error),
#[error("Error while awaiting index creation task")]
Task,
}
// The chunk size for adding projects to the indexing database. If the request size
@@ -57,8 +58,8 @@ pub async fn index_projects(
if settings.index_local {
docs_to_add.append(&mut index_local(pool.clone()).await?);
}
// Write Indices
// Write Indices
add_projects(docs_to_add, config).await?;
Ok(())
@@ -67,122 +68,91 @@ pub async fn index_projects(
pub async fn reset_indices(config: &SearchConfig) -> Result<(), IndexingError> {
let client = config.make_client();
client.delete_index("relevance_projects").await?;
client.delete_index("downloads_projects").await?;
client.delete_index("follows_projects").await?;
client.delete_index("updated_projects").await?;
client.delete_index("newest_projects").await?;
Ok(())
}
async fn update_index_helper<'a>(
client: &'a Client<'a>,
name: &'static str,
rule: &'static str,
) -> Result<Index<'a>, IndexingError> {
update_index(client, name, {
let mut rules = default_rules();
rules.push_back(rule);
rules.into()
})
.await
}
pub async fn reconfigure_indices(
config: &SearchConfig,
) -> Result<(), IndexingError> {
let client = config.make_client();
// Relevance Index
update_index_helper(&client, "relevance_projects", "desc(downloads)")
.await?;
update_index_helper(&client, "downloads_projects", "desc(downloads)")
.await?;
update_index_helper(&client, "follows_projects", "desc(follows)").await?;
update_index_helper(
&client,
"updated_projects",
"desc(modified_timestamp)",
)
.await?;
update_index_helper(&client, "newest_projects", "desc(created_timestamp)")
.await?;
client.delete_index("projects").await?;
client.delete_index("projects_filtered").await?;
Ok(())
}
async fn update_index<'a>(
client: &'a Client<'a>,
name: &'a str,
rules: Vec<&'static str>,
) -> Result<Index<'a>, IndexingError> {
let index = match client.get_index(name).await {
Ok(index) => index,
Err(meilisearch_sdk::errors::Error::MeiliSearchError {
error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound,
..
}) => client.create_index(name, Some("project_id")).await?,
Err(e) => {
return Err(IndexingError::IndexDBError(e));
}
};
index
.set_settings(&default_settings().with_ranking_rules(rules))
.await?;
Ok(index)
}
async fn create_index<'a>(
client: &'a Client<'a>,
async fn create_index(
client: &Client,
name: &'static str,
rules: impl FnOnce() -> Vec<&'static str>,
) -> Result<Index<'a>, IndexingError> {
custom_rules: Option<&'static [&'static str]>,
) -> Result<Index, IndexingError> {
client
.delete_index(name)
.await?
.wait_for_completion(client, None, None)
.await?;
match client.get_index(name).await {
// TODO: update index settings on startup (or delete old indices on startup)
Ok(index) => Ok(index),
Err(meilisearch_sdk::errors::Error::MeiliSearchError {
error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound,
..
}) => {
Ok(index) => {
index
.set_settings(&default_settings())
.await?
.wait_for_completion(client, None, None)
.await?;
Ok(index)
}
Err(meilisearch_sdk::errors::Error::Meilisearch(
meilisearch_sdk::errors::MeilisearchError {
error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound,
..
},
)) => {
// Only create index and set settings if the index doesn't already exist
let index = client.create_index(name, Some("project_id")).await?;
let task = client.create_index(name, Some("project_id")).await?;
let task = task.wait_for_completion(client, None, None).await?;
let index = task
.try_make_index(client)
.map_err(|_| IndexingError::Task)?;
let mut settings = default_settings();
if let Some(custom_rules) = custom_rules {
settings = settings.with_ranking_rules(custom_rules);
}
index
.set_settings(&default_settings().with_ranking_rules(rules()))
.set_settings(&settings)
.await?
.wait_for_completion(client, None, None)
.await?;
Ok(index)
}
Err(e) => {
log::warn!("Unhandled error while creating index: {}", e);
Err(IndexingError::IndexDBError(e))
Err(IndexingError::Indexing(e))
}
}
}
async fn add_to_index(
index: Index<'_>,
client: &Client,
index: Index,
mods: &[UploadSearchProject],
) -> Result<(), IndexingError> {
for chunk in mods.chunks(MEILISEARCH_CHUNK_SIZE) {
index.add_documents(chunk, Some("project_id")).await?;
index
.add_documents(chunk, Some("project_id"))
.await?
.wait_for_completion(client, None, None)
.await?;
}
Ok(())
}
async fn create_and_add_to_index<'a>(
client: &'a Client<'a>,
projects: &'a [UploadSearchProject],
async fn create_and_add_to_index(
client: &Client,
projects: &[UploadSearchProject],
name: &'static str,
rule: &'static str,
custom_rules: Option<&'static [&'static str]>,
) -> Result<(), IndexingError> {
let index = create_index(client, name, || {
let mut relevance_rules = default_rules();
relevance_rules.push_back(rule);
relevance_rules.into()
})
.await?;
add_to_index(index, projects).await?;
let index = create_index(client, name, custom_rules).await?;
add_to_index(client, index, projects).await?;
Ok(())
}
@@ -192,65 +162,32 @@ pub async fn add_projects(
) -> Result<(), IndexingError> {
let client = config.make_client();
create_and_add_to_index(&client, &projects, "projects", None).await?;
create_and_add_to_index(
&client,
&projects,
"relevance_projects",
"desc(downloads)",
)
.await?;
create_and_add_to_index(
&client,
&projects,
"downloads_projects",
"desc(downloads)",
)
.await?;
create_and_add_to_index(
&client,
&projects,
"follows_projects",
"desc(follows)",
)
.await?;
create_and_add_to_index(
&client,
&projects,
"updated_projects",
"desc(modified_timestamp)",
)
.await?;
create_and_add_to_index(
&client,
&projects,
"newest_projects",
"desc(created_timestamp)",
"projects_filtered",
Some(&[
"sort",
"words",
"typo",
"proximity",
"attribute",
"exactness",
]),
)
.await?;
Ok(())
}
//region Utils
fn default_rules() -> VecDeque<&'static str> {
vec![
"typo",
"words",
"proximity",
"attribute",
"wordsPosition",
"exactness",
]
.into()
}
fn default_settings() -> Settings {
Settings::new()
.with_displayed_attributes(DEFAULT_DISPLAYED_ATTRIBUTES)
.with_searchable_attributes(DEFAULT_SEARCHABLE_ATTRIBUTES)
.with_stop_words(Vec::<String>::new())
.with_synonyms(HashMap::<String, Vec<String>>::new())
.with_attributes_for_faceting(DEFAULT_ATTRIBUTES_FOR_FACETING)
.with_sortable_attributes(DEFAULT_SORTABLE_ATTRIBUTES)
.with_filterable_attributes(DEFAULT_ATTRIBUTES_FOR_FACETING)
}
const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[
@@ -275,72 +212,22 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[
];
const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] =
&["title", "description", "categories", "versions", "author"];
&["title", "description", "author"];
const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[
"categories",
"host",
"versions",
"license",
"client_side",
"server_side",
"project_type",
"downloads",
"follows",
"author",
"title",
"date_created",
"date_modified",
];
//endregion
// This shouldn't be relied on for proper sorting, but it makes an
// attempt at getting proper sorting for Mojang's versions.
// This isn't currently used, but I wrote it and it works, so I'm
// keeping this mess in case someone needs it in the future.
#[allow(dead_code)]
pub fn sort_projects(a: &str, b: &str) -> std::cmp::Ordering {
use std::cmp::Ordering;
let cmp = a.contains('.').cmp(&b.contains('.'));
if cmp != Ordering::Equal {
return cmp;
}
let mut a = a.split(&['.', '-'] as &[char]);
let mut b = b.split(&['.', '-'] as &[char]);
let a = (a.next(), a.next(), a.next(), a.next());
let b = (b.next(), b.next(), b.next(), b.next());
if a.0 == b.0 {
let cmp =
a.1.map(|s| s.chars().all(|c| c.is_ascii_digit()))
.cmp(&b.1.map(|s| s.chars().all(|c| c.is_ascii_digit())));
if cmp != Ordering::Equal {
return cmp;
}
if a.1 == b.1 {
let cmp =
a.2.map(|s| s.chars().all(|c| c.is_ascii_digit()))
.unwrap_or(true)
.cmp(
&b.2.map(|s| s.chars().all(|c| c.is_ascii_digit()))
.unwrap_or(true),
);
if cmp != Ordering::Equal {
return cmp;
}
if a.2 == b.2 {
match (a.3.is_some(), b.3.is_some()) {
(false, false) => Ordering::Equal,
(false, true) => Ordering::Greater,
(true, false) => Ordering::Less,
(true, true) => a.3.cmp(&b.3),
}
} else {
a.2.cmp(&b.2)
}
} else {
a.1.cmp(&b.1)
}
} else {
match (a.0 == Some("1"), b.0 == Some("1")) {
(false, false) => a.0.cmp(&b.0),
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(true, true) => unreachable!(),
}
}
}
const DEFAULT_SORTABLE_ATTRIBUTES: &[&str] =
&["downloads", "follows", "date_created", "date_modified"];

View File

@@ -15,13 +15,13 @@ pub mod indexing;
#[derive(Error, Debug)]
pub enum SearchError {
#[error("MeiliSearch Error: {0}")]
MeiliSearchError(#[from] meilisearch_sdk::errors::Error),
MeiliSearch(#[from] meilisearch_sdk::errors::Error),
#[error("Error while serializing or deserializing JSON: {0}")]
SerdeError(#[from] serde_json::Error),
Serde(#[from] serde_json::Error),
#[error("Error while parsing an integer: {0}")]
IntParsingError(#[from] std::num::ParseIntError),
IntParsing(#[from] std::num::ParseIntError),
#[error("Environment Error")]
EnvError(#[from] dotenv::Error),
Env(#[from] dotenv::Error),
#[error("Invalid index to sort by: {0}")]
InvalidIndex(String),
}
@@ -29,10 +29,10 @@ pub enum SearchError {
impl actix_web::ResponseError for SearchError {
fn status_code(&self) -> StatusCode {
match self {
SearchError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR,
SearchError::MeiliSearchError(..) => StatusCode::BAD_REQUEST,
SearchError::SerdeError(..) => StatusCode::BAD_REQUEST,
SearchError::IntParsingError(..) => StatusCode::BAD_REQUEST,
SearchError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
SearchError::MeiliSearch(..) => StatusCode::BAD_REQUEST,
SearchError::Serde(..) => StatusCode::BAD_REQUEST,
SearchError::IntParsing(..) => StatusCode::BAD_REQUEST,
SearchError::InvalidIndex(..) => StatusCode::BAD_REQUEST,
}
}
@@ -40,10 +40,10 @@ impl actix_web::ResponseError for SearchError {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).json(ApiError {
error: match self {
SearchError::EnvError(..) => "environment_error",
SearchError::MeiliSearchError(..) => "meilisearch_error",
SearchError::SerdeError(..) => "invalid_input",
SearchError::IntParsingError(..) => "invalid_input",
SearchError::Env(..) => "environment_error",
SearchError::MeiliSearch(..) => "meilisearch_error",
SearchError::Serde(..) => "invalid_input",
SearchError::IntParsing(..) => "invalid_input",
SearchError::InvalidIndex(..) => "invalid_input",
},
description: &self.to_string(),
@@ -149,62 +149,85 @@ pub async fn search_for_project(
) -> Result<SearchResults, SearchError> {
let client = Client::new(&*config.address, &*config.key);
let filters: Cow<_> =
match (info.filters.as_deref(), info.version.as_deref()) {
(Some(f), Some(v)) => format!("({}) AND ({})", f, v).into(),
(Some(f), None) => f.into(),
(None, Some(v)) => v.into(),
(None, None) => "".into(),
};
let offset = info.offset.as_deref().unwrap_or("0").parse()?;
let index = info.index.as_deref().unwrap_or("relevance");
let limit = info.limit.as_deref().unwrap_or("10").parse()?;
let index = match index {
"relevance" => "relevance_projects",
"downloads" => "downloads_projects",
"follows" => "follows_projects",
"updated" => "updated_projects",
"newest" => "newest_projects",
let sort = match index {
"relevance" => ("projects", ["downloads:desc"]),
"downloads" => ("projects_filtered", ["downloads:desc"]),
"follows" => ("projects_filtered", ["follows:desc"]),
"updated" => ("projects_filtered", ["date_created:desc"]),
"newest" => ("projects_filtered", ["date_modified:desc"]),
i => return Err(SearchError::InvalidIndex(i.to_string())),
};
let meilisearch_index = client.get_index(index).await?;
let mut query = meilisearch_index.search();
let meilisearch_index = client.get_index(sort.0).await?;
query.with_limit(min(100, limit)).with_offset(offset);
let mut filter_string = String::new();
if let Some(search) = info.query.as_deref() {
if !search.is_empty() {
query.with_query(search);
let results = {
let mut query = meilisearch_index.search();
query
.with_limit(min(100, limit))
.with_offset(offset)
.with_query(info.query.as_deref().unwrap_or_default())
.with_sort(&sort.1);
if let Some(new_filters) = info.new_filters.as_deref() {
query.with_filter(new_filters);
} else {
let facets = if let Some(facets) = &info.facets {
Some(serde_json::from_str::<Vec<Vec<&str>>>(facets)?)
} else {
None
};
let filters: Cow<_> =
match (info.filters.as_deref(), info.version.as_deref()) {
(Some(f), Some(v)) => format!("({}) AND ({})", f, v).into(),
(Some(f), None) => f.into(),
(None, Some(v)) => v.into(),
(None, None) => "".into(),
};
if let Some(facets) = facets {
filter_string.push('(');
for (index, facet_list) in facets.iter().enumerate() {
filter_string.push('(');
for (facet_index, facet) in facet_list.iter().enumerate() {
filter_string.push_str(&facet.replace(':', " = "));
if facet_index != (facet_list.len() - 1) {
filter_string.push_str(" OR ")
}
}
filter_string.push(')');
if index != (facets.len() - 1) {
filter_string.push_str(" AND ")
}
}
filter_string.push(')');
if !filters.is_empty() {
filter_string.push_str(&format!(" AND ({})", filter_string))
}
} else {
filter_string.push_str(&*filters);
}
println!("{}", filter_string);
if !filter_string.is_empty() {
query.with_filter(&filter_string);
}
}
}
if !filters.is_empty() {
query.with_filters(&filters);
}
// So the meilisearch sdk's lifetimes are... broken, to say the least
// They are overspecified and almost always wrong, and would generally
// just be better if they didn't specify them at all.
// They also decided to have this take a &[&[&str]], which is impossible
// to construct efficiently. Instead it should take impl Iterator<Item=&[&str]>,
// &[impl AsRef<[&str]>], or one of many other proper solutions to that issue.
let why_meilisearch;
let why_must_you_do_this;
if let Some(facets) = &info.facets {
why_meilisearch = serde_json::from_str::<Vec<Vec<&str>>>(facets)?;
why_must_you_do_this = why_meilisearch
.iter()
.map(|v| v as &[_])
.collect::<Vec<&[_]>>();
query.with_facet_filters(&why_must_you_do_this);
}
let results = query.execute::<ResultSearchProject>().await?;
query.execute::<ResultSearchProject>().await?
};
Ok(SearchResults {
hits: results.hits.into_iter().map(|r| r.result).collect(),

View File

@@ -12,15 +12,15 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum AuthenticationError {
#[error("An unknown database error occurred")]
SqlxDatabaseError(#[from] sqlx::Error),
Sqlx(#[from] sqlx::Error),
#[error("Database Error: {0}")]
DatabaseError(#[from] crate::database::models::DatabaseError),
Database(#[from] crate::database::models::DatabaseError),
#[error("Error while parsing JSON: {0}")]
SerdeError(#[from] serde_json::Error),
SerDe(#[from] serde_json::Error),
#[error("Error while communicating to GitHub OAuth2: {0}")]
GithubError(#[from] reqwest::Error),
Github(#[from] reqwest::Error),
#[error("Invalid Authentication Credentials")]
InvalidCredentialsError,
InvalidCredentials,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -73,7 +73,7 @@ where
created: result.created,
role: Role::from_string(&result.role),
}),
None => Err(AuthenticationError::InvalidCredentialsError),
None => Err(AuthenticationError::InvalidCredentials),
}
}
pub async fn get_user_from_headers<'a, 'b, E>(
@@ -85,9 +85,9 @@ where
{
let token = headers
.get("Authorization")
.ok_or(AuthenticationError::InvalidCredentialsError)?
.ok_or(AuthenticationError::InvalidCredentials)?
.to_str()
.map_err(|_| AuthenticationError::InvalidCredentialsError)?;
.map_err(|_| AuthenticationError::InvalidCredentials)?;
Ok(get_user_from_token(token, executor).await?)
}
@@ -104,7 +104,7 @@ where
if user.role.is_mod() {
Ok(user)
} else {
Err(AuthenticationError::InvalidCredentialsError)
Err(AuthenticationError::InvalidCredentials)
}
}
@@ -119,7 +119,7 @@ where
match user.role {
Role::Admin => Ok(user),
_ => Err(AuthenticationError::InvalidCredentialsError),
_ => Err(AuthenticationError::InvalidCredentials),
}
}

View File

@@ -15,10 +15,10 @@ pub async fn read_from_payload(
let mut bytes = BytesMut::new();
while let Some(item) = payload.next().await {
if bytes.len() >= cap {
return Err(ApiError::InvalidInputError(String::from(err_msg)));
return Err(ApiError::InvalidInput(String::from(err_msg)));
} else {
bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError(
ApiError::InvalidInput(
"Unable to parse bytes in payload sent!".to_string(),
)
})?);

View File

@@ -34,7 +34,7 @@ pub fn validation_errors_to_string(
*errors.clone(),
Some(format!(
"of list {} with index {}",
index, field
field, index
)),
));
}

View File

@@ -33,7 +33,7 @@ impl super::Validator for FabricValidator {
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
) -> Result<ValidationResult, ValidationError> {
archive.by_name("fabric.mod.json").map_err(|_| {
ValidationError::InvalidInputError(
ValidationError::InvalidInput(
"No fabric.mod.json present for Fabric file.".into(),
)
})?;

View File

@@ -33,7 +33,7 @@ impl super::Validator for ForgeValidator {
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
) -> Result<ValidationResult, ValidationError> {
archive.by_name("META-INF/mods.toml").map_err(|_| {
ValidationError::InvalidInputError(
ValidationError::InvalidInput(
"No mods.toml present for Forge file.".into(),
)
})?;

View File

@@ -14,15 +14,15 @@ mod pack;
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("Unable to read Zip Archive: {0}")]
ZipError(#[from] zip::result::ZipError),
Zip(#[from] zip::result::ZipError),
#[error("IO Error: {0}")]
IoError(#[from] std::io::Error),
Io(#[from] std::io::Error),
#[error("Error while validating JSON: {0}")]
SerdeError(#[from] serde_json::Error),
SerDe(#[from] serde_json::Error),
#[error("Invalid Input: {0}")]
InvalidInputError(std::borrow::Cow<'static, str>),
InvalidInput(std::borrow::Cow<'static, str>),
#[error("Error while managing threads")]
BlockingError(#[from] actix_web::error::BlockingError),
Blocking(#[from] actix_web::error::BlockingError),
}
#[derive(Eq, PartialEq)]
@@ -93,7 +93,7 @@ pub async fn validate_file(
}
if visited {
Err(ValidationError::InvalidInputError(
Err(ValidationError::InvalidInput(
format!(
"File extension {} is invalid for input file",
file_extension

View File

@@ -138,7 +138,7 @@ impl super::Validator for PackValidator {
) -> Result<ValidationResult, ValidationError> {
let mut file =
archive.by_name("modrinth.index.json").map_err(|_| {
ValidationError::InvalidInputError(
ValidationError::InvalidInput(
"Pack manifest is missing.".into(),
)
})?;
@@ -149,20 +149,20 @@ impl super::Validator for PackValidator {
let pack: PackFormat = serde_json::from_str(&contents)?;
pack.validate().map_err(|err| {
ValidationError::InvalidInputError(
ValidationError::InvalidInput(
validation_errors_to_string(err, None).into(),
)
})?;
if pack.game != "minecraft" {
return Err(ValidationError::InvalidInputError(
return Err(ValidationError::InvalidInput(
format!("Game {0} does not exist!", pack.game).into(),
));
}
for file in pack.files {
if file.hashes.get(&FileHash::Sha1).is_none() {
return Err(ValidationError::InvalidInputError(
return Err(ValidationError::InvalidInput(
"All pack files must provide a SHA1 hash!".into(),
));
}
@@ -171,7 +171,7 @@ impl super::Validator for PackValidator {
.components()
.next()
.ok_or_else(|| {
ValidationError::InvalidInputError(
ValidationError::InvalidInput(
"Invalid pack file path!".into(),
)
})?;
@@ -179,7 +179,7 @@ impl super::Validator for PackValidator {
match path {
Component::CurDir | Component::Normal(_) => {}
_ => {
return Err(ValidationError::InvalidInputError(
return Err(ValidationError::InvalidInput(
"Invalid pack file path!".into(),
))
}