You've already forked AstralRinth
forked from didirus/AstralRinth
Shulkers of fixes (#327)
* Shulkers of fixes * Fix validation message * Update deps * Bump docker image version
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
18
src/main.rs
18
src/main.rs
@@ -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)?
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)),
|
||||
)))
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)))
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)))
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
})?);
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn validation_errors_to_string(
|
||||
*errors.clone(),
|
||||
Some(format!(
|
||||
"of list {} with index {}",
|
||||
index, field
|
||||
field, index
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user