Add dependencies to search (#578)

* Add dependencies to search

* add attrs for faceting

* run prepare

* Add user data route from token

* update to 24hrs

* Fix report bugs
This commit is contained in:
Geometrically
2023-04-20 16:38:30 -07:00
committed by GitHub
parent 5c559af936
commit 59f24df294
65 changed files with 1518 additions and 2218 deletions

View File

@@ -68,11 +68,8 @@ pub async fn maven_metadata(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let project_id = params.into_inner().0;
let project_data = database::models::Project::get_from_slug_or_project_id(
&project_id,
&**pool,
)
.await?;
let project_data =
database::models::Project::get_from_slug_or_project_id(&project_id, &**pool).await?;
let data = if let Some(data) = project_data {
data
@@ -150,9 +147,7 @@ fn find_file<'a>(
version: &'a QueryVersion,
file: &str,
) -> Option<&'a QueryFile> {
if let Some(selected_file) =
version.files.iter().find(|x| x.filename == file)
{
if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) {
return Some(selected_file);
}
@@ -193,11 +188,7 @@ pub async fn version_file(
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let project_data =
database::models::Project::get_full_from_slug_or_project_id(
&project_id,
&**pool,
)
.await?;
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
let project = if let Some(data) = project_data {
data
@@ -229,11 +220,9 @@ pub async fn version_file(
return Ok(HttpResponse::NotFound().body(""));
};
let version = if let Some(version) = database::models::Version::get_full(
database::models::ids::VersionId(vid.id),
&**pool,
)
.await?
let version = if let Some(version) =
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
.await?
{
version
} else {
@@ -263,9 +252,7 @@ pub async fn version_file(
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)
{
} else if let Some(selected_file) = find_file(&project_id, &project, &version, &file) {
return Ok(HttpResponse::TemporaryRedirect()
.append_header(("location", &*selected_file.url))
.body(""));
@@ -282,11 +269,7 @@ pub async fn version_file_sha1(
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let project_data =
database::models::Project::get_full_from_slug_or_project_id(
&project_id,
&**pool,
)
.await?;
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
let project = if let Some(data) = project_data {
data
@@ -318,11 +301,9 @@ pub async fn version_file_sha1(
return Ok(HttpResponse::NotFound().body(""));
};
let version = if let Some(version) = database::models::Version::get_full(
database::models::ids::VersionId(vid.id),
&**pool,
)
.await?
let version = if let Some(version) =
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
.await?
{
version
} else {
@@ -343,11 +324,7 @@ pub async fn version_file_sha512(
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let project_data =
database::models::Project::get_full_from_slug_or_project_id(
&project_id,
&**pool,
)
.await?;
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
let project = if let Some(data) = project_data {
data
@@ -379,11 +356,9 @@ pub async fn version_file_sha512(
return Ok(HttpResponse::NotFound().body(""));
};
let version = if let Some(version) = database::models::Version::get_full(
database::models::ids::VersionId(vid.id),
&**pool,
)
.await?
let version = if let Some(version) =
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
.await?
{
version
} else {

View File

@@ -97,30 +97,28 @@ impl actix_web::ResponseError for ApiError {
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).json(
crate::models::error::ApiError {
error: match self {
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",
ApiError::Analytics(..) => "analytics_error",
ApiError::Crypto(..) => "crypto_error",
ApiError::Payments(..) => "payments_error",
ApiError::DiscordError(..) => "discord_error",
ApiError::Decoding(..) => "decoding_error",
ApiError::ImageError(..) => "invalid_image",
},
description: &self.to_string(),
HttpResponse::build(self.status_code()).json(crate::models::error::ApiError {
error: match self {
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",
ApiError::Analytics(..) => "analytics_error",
ApiError::Crypto(..) => "crypto_error",
ApiError::Payments(..) => "payments_error",
ApiError::DiscordError(..) => "discord_error",
ApiError::Decoding(..) => "decoding_error",
ApiError::ImageError(..) => "invalid_image",
},
)
description: &self.to_string(),
})
}
}

View File

@@ -6,9 +6,7 @@ use sqlx::PgPool;
use crate::database;
use crate::models::projects::VersionType;
use crate::util::auth::{
filter_authorized_versions, get_user_from_headers, is_authorized,
};
use crate::util::auth::{filter_authorized_versions, get_user_from_headers, is_authorized};
use super::ApiError;
@@ -26,10 +24,9 @@ pub async fn forge_updates(
let (id,) = info.into_inner();
let project =
database::models::Project::get_from_slug_or_project_id(&id, &**pool)
.await?
.ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
let project = database::models::Project::get_from_slug_or_project_id(&id, &**pool)
.await?
.ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -48,11 +45,9 @@ pub async fn forge_updates(
)
.await?;
let versions =
database::models::Version::get_many_full(&version_ids, &**pool).await?;
let versions = database::models::Version::get_many_full(&version_ids, &**pool).await?;
let mut versions =
filter_authorized_versions(versions, &user_option, &pool).await?;
let mut versions = filter_authorized_versions(versions, &user_option, &pool).await?;
versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));

View File

@@ -37,26 +37,22 @@ pub async fn count_download(
download_body: web::Json<DownloadBody>,
download_queue: web::Data<Arc<DownloadQueue>>,
) -> Result<HttpResponse, ApiError> {
let project_id: crate::database::models::ids::ProjectId =
download_body.project_id.into();
let project_id: crate::database::models::ids::ProjectId = download_body.project_id.into();
let id_option = crate::models::ids::base62_impl::parse_base62(
&download_body.version_name,
)
.ok()
.map(|x| x as i64);
let id_option = crate::models::ids::base62_impl::parse_base62(&download_body.version_name)
.ok()
.map(|x| x as i64);
let (version_id, project_id, file_type) = if let Some(version) =
sqlx::query!(
"
let (version_id, project_id, file_type) = if let Some(version) = sqlx::query!(
"
SELECT v.id id, v.mod_id mod_id, file_type FROM files f
INNER JOIN versions v ON v.id = f.version_id
WHERE f.url = $1
",
download_body.url,
)
.fetch_optional(pool.as_ref())
.await?
download_body.url,
)
.fetch_optional(pool.as_ref())
.await?
{
(version.id, version.mod_id, version.file_type)
} else if let Some(version) = sqlx::query!(
@@ -143,17 +139,11 @@ pub async fn process_payout(
)])
.send()
.await
.map_err(|_| {
ApiError::Analytics(
"Error while fetching payout multipliers!".to_string(),
)
})?
.map_err(|_| ApiError::Analytics("Error while fetching payout multipliers!".to_string()))?
.json()
.await
.map_err(|_| {
ApiError::Analytics(
"Error while deserializing payout multipliers!".to_string(),
)
ApiError::Analytics("Error while deserializing payout multipliers!".to_string())
})?;
struct Project {
@@ -176,26 +166,33 @@ pub async fn process_payout(
INNER JOIN project_types pt ON pt.id = m.project_type
WHERE m.id = ANY($1) AND m.monetization_status = $2
",
&multipliers.values.keys().flat_map(|x| x.parse::<i64>().ok()).collect::<Vec<i64>>(),
&multipliers
.values
.keys()
.flat_map(|x| x.parse::<i64>().ok())
.collect::<Vec<i64>>(),
MonetizationStatus::Monetized.as_str(),
)
.fetch_many(&mut *transaction)
.try_for_each(|e| {
if let Some(row) = e.right() {
if let Some(project) = projects_map.get_mut(&row.id) {
project.team_members.push((row.user_id, row.payouts_split));
} else {
projects_map.insert(row.id, Project {
.fetch_many(&mut *transaction)
.try_for_each(|e| {
if let Some(row) = e.right() {
if let Some(project) = projects_map.get_mut(&row.id) {
project.team_members.push((row.user_id, row.payouts_split));
} else {
projects_map.insert(
row.id,
Project {
project_type: row.project_type,
team_members: vec![(row.user_id, row.payouts_split)],
split_team_members: Default::default()
});
}
split_team_members: Default::default(),
},
);
}
}
futures::future::ready(Ok(()))
})
.await?;
futures::future::ready(Ok(()))
})
.await?;
// Specific Payout Conditions (ex: modpack payout split)
let mut projects_split_dependencies = Vec::new();
@@ -208,8 +205,7 @@ pub async fn process_payout(
if !projects_split_dependencies.is_empty() {
// (dependent_id, (dependency_id, times_depended))
let mut project_dependencies: HashMap<i64, Vec<(i64, i64)>> =
HashMap::new();
let mut project_dependencies: HashMap<i64, Vec<(i64, i64)>> = HashMap::new();
// dependency_ids to fetch team members from
let mut fetch_team_members: Vec<i64> = Vec::new();
@@ -229,14 +225,11 @@ pub async fn process_payout(
if let Some(row) = e.right() {
fetch_team_members.push(row.id);
if let Some(project) = project_dependencies.get_mut(&row.mod_id)
{
if let Some(project) = project_dependencies.get_mut(&row.mod_id) {
project.push((row.id, row.times_depended.unwrap_or(0)));
} else {
project_dependencies.insert(
row.mod_id,
vec![(row.id, row.times_depended.unwrap_or(0))],
);
project_dependencies
.insert(row.mod_id, vec![(row.id, row.times_depended.unwrap_or(0))]);
}
}
@@ -245,8 +238,7 @@ pub async fn process_payout(
.await?;
// (project_id, (user_id, payouts_split))
let mut team_members: HashMap<i64, Vec<(i64, Decimal)>> =
HashMap::new();
let mut team_members: HashMap<i64, Vec<(i64, Decimal)>> = HashMap::new();
sqlx::query!(
"
@@ -263,8 +255,7 @@ pub async fn process_payout(
if let Some(project) = team_members.get_mut(&row.id) {
project.push((row.user_id, row.payouts_split));
} else {
team_members
.insert(row.id, vec![(row.user_id, row.payouts_split)]);
team_members.insert(row.id, vec![(row.user_id, row.payouts_split)]);
}
}
@@ -281,17 +272,14 @@ pub async fn process_payout(
if dep_sum > 0 {
for dependency in dependencies {
let project_multiplier: Decimal =
Decimal::from(dependency.1)
/ Decimal::from(dep_sum);
Decimal::from(dependency.1) / Decimal::from(dep_sum);
if let Some(members) = team_members.get(&dependency.0) {
let members_sum: Decimal =
members.iter().map(|x| x.1).sum();
let members_sum: Decimal = members.iter().map(|x| x.1).sum();
if members_sum > Decimal::ZERO {
for member in members {
let member_multiplier: Decimal =
member.1 / members_sum;
let member_multiplier: Decimal = member.1 / members_sum;
project.split_team_members.push((
member.0,
member_multiplier * project_multiplier,
@@ -315,10 +303,8 @@ pub async fn process_payout(
let split_given = Decimal::ONE / Decimal::from(5);
let split_retention = Decimal::from(4) / Decimal::from(5);
let sum_splits: Decimal =
project.team_members.iter().map(|x| x.1).sum();
let sum_tm_splits: Decimal =
project.split_team_members.iter().map(|x| x.1).sum();
let sum_splits: Decimal = project.team_members.iter().map(|x| x.1).sum();
let sum_tm_splits: Decimal = project.split_team_members.iter().map(|x| x.1).sum();
if sum_splits > Decimal::ZERO {
for (user_id, split) in project.team_members {
@@ -342,8 +328,8 @@ pub async fn process_payout(
payout,
start
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
sqlx::query!(
"
@@ -378,8 +364,8 @@ pub async fn process_payout(
payout,
start
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
sqlx::query!(
"

View File

@@ -58,12 +58,8 @@ impl actix_web::ResponseError for AuthorizationError {
fn status_code(&self) -> StatusCode {
match self {
AuthorizationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::SqlxDatabase(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthorizationError::Database(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthorizationError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST,
AuthorizationError::Github(..) => StatusCode::FAILED_DEPENDENCY,
AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED,
@@ -84,9 +80,7 @@ impl actix_web::ResponseError for AuthorizationError {
AuthorizationError::Github(..) => "github_error",
AuthorizationError::InvalidCredentials => "invalid_credentials",
AuthorizationError::Decoding(..) => "decoding_error",
AuthorizationError::Authentication(..) => {
"authentication_error"
}
AuthorizationError::Authentication(..) => "authentication_error",
AuthorizationError::Url => "url_error",
AuthorizationError::Banned => "user_banned",
},
@@ -119,16 +113,12 @@ pub async fn init(
Query(info): Query<AuthorizationInit>,
client: Data<PgPool>,
) -> Result<HttpResponse, AuthorizationError> {
let url =
url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?;
let url = url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?;
let allowed_callback_urls =
parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
let domain = url.domain().ok_or(AuthorizationError::Url)?;
if !allowed_callback_urls.iter().any(|x| domain.ends_with(x))
&& domain != "modrinth.com"
{
if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) && domain != "modrinth.com" {
return Err(AuthorizationError::Url);
}
@@ -215,8 +205,7 @@ pub async fn auth_callback(
let user = get_github_user_from_token(&token.access_token).await?;
let user_result =
User::get_from_github_id(user.id, &mut *transaction).await?;
let user_result = User::get_from_github_id(user.id, &mut *transaction).await?;
match user_result {
Some(_) => {}
None => {
@@ -231,9 +220,7 @@ pub async fn auth_callback(
return Err(AuthorizationError::Banned);
}
let user_id =
crate::database::models::generate_user_id(&mut transaction)
.await?;
let user_id = crate::database::models::generate_user_id(&mut transaction).await?;
let mut username_increment: i32 = 0;
let mut username = None;

View File

@@ -54,17 +54,11 @@ pub async fn init_checkout(
])
.send()
.await
.map_err(|_| {
ApiError::Payments(
"Error while creating checkout session!".to_string(),
)
})?
.map_err(|_| ApiError::Payments("Error while creating checkout session!".to_string()))?
.json::<Session>()
.await
.map_err(|_| {
ApiError::Payments(
"Error while deserializing checkout response!".to_string(),
)
ApiError::Payments("Error while deserializing checkout response!".to_string())
})?;
Ok(HttpResponse::Ok().json(json!(
@@ -92,11 +86,7 @@ pub async fn init_customer_portal(
.fetch_optional(&**pool)
.await?
.and_then(|x| x.stripe_customer_id)
.ok_or_else(|| {
ApiError::InvalidInput(
"User is not linked to stripe account!".to_string(),
)
})?;
.ok_or_else(|| ApiError::InvalidInput("User is not linked to stripe account!".to_string()))?;
let client = reqwest::Client::new();
@@ -117,17 +107,11 @@ pub async fn init_customer_portal(
])
.send()
.await
.map_err(|_| {
ApiError::Payments(
"Error while creating billing session!".to_string(),
)
})?
.map_err(|_| ApiError::Payments("Error while creating billing session!".to_string()))?
.json::<Session>()
.await
.map_err(|_| {
ApiError::Payments(
"Error while deserializing billing response!".to_string(),
)
ApiError::Payments("Error while deserializing billing response!".to_string())
})?;
Ok(HttpResponse::Ok().json(json!(
@@ -166,27 +150,25 @@ pub async fn handle_stripe_webhook(
if let Some(signature) = signature {
type HmacSha256 = Hmac<sha2::Sha256>;
let mut key = HmacSha256::new_from_slice(dotenvy::var("STRIPE_WEBHOOK_SECRET")?.as_bytes()).map_err(|_| {
ApiError::Crypto(
"Unable to initialize HMAC instance due to invalid key length!".to_string(),
)
})?;
let mut key =
HmacSha256::new_from_slice(dotenvy::var("STRIPE_WEBHOOK_SECRET")?.as_bytes())
.map_err(|_| {
ApiError::Crypto(
"Unable to initialize HMAC instance due to invalid key length!"
.to_string(),
)
})?;
key.update(format!("{timestamp}.{body}").as_bytes());
key.verify(&signature).map_err(|_| {
ApiError::Crypto(
"Unable to verify webhook signature!".to_string(),
)
ApiError::Crypto("Unable to verify webhook signature!".to_string())
})?;
if timestamp < (Utc::now() - Duration::minutes(5)).timestamp()
|| timestamp
> (Utc::now() + Duration::minutes(5)).timestamp()
|| timestamp > (Utc::now() + Duration::minutes(5)).timestamp()
{
return Err(ApiError::Crypto(
"Webhook signature expired!".to_string(),
));
return Err(ApiError::Crypto("Webhook signature expired!".to_string()));
}
} else {
return Err(ApiError::Crypto("Missing signature!".to_string()));
@@ -256,8 +238,7 @@ pub async fn handle_stripe_webhook(
// TODO: Currently hardcoded to midas-only. When we add more stuff should include price IDs
match &*webhook.type_ {
"checkout.session.completed" => {
let session: CheckoutSession =
serde_json::from_value(webhook.data.object)?;
let session: CheckoutSession = serde_json::from_value(webhook.data.object)?;
sqlx::query!(
"
@@ -276,8 +257,7 @@ pub async fn handle_stripe_webhook(
if let Some(item) = invoice.lines.data.first() {
let expires: DateTime<Utc> = DateTime::from_utc(
NaiveDateTime::from_timestamp_opt(item.period.end, 0)
.unwrap_or_default(),
NaiveDateTime::from_timestamp_opt(item.period.end, 0).unwrap_or_default(),
Utc,
) + Duration::days(1);
@@ -323,8 +303,7 @@ pub async fn handle_stripe_webhook(
}
}
"customer.subscription.deleted" => {
let session: Subscription =
serde_json::from_value(webhook.data.object)?;
let session: Subscription = serde_json::from_value(webhook.data.object)?;
sqlx::query!(
"
@@ -334,8 +313,8 @@ pub async fn handle_stripe_webhook(
",
session.customer,
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
}
_ => {}
};

View File

@@ -46,18 +46,15 @@ pub async fn get_projects(
count.count as i64
)
.fetch_many(&**pool)
.try_filter_map(|e| async {
Ok(e.right().map(|m| database::models::ProjectId(m.id)))
})
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ProjectId(m.id))) })
.try_collect::<Vec<database::models::ProjectId>>()
.await?;
let projects: Vec<_> =
database::Project::get_many_full(&project_ids, &**pool)
.await?
.into_iter()
.map(crate::models::projects::Project::from)
.collect();
let projects: Vec<_> = database::Project::get_many_full(&project_ids, &**pool)
.await?
.into_iter()
.map(crate::models::projects::Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects))
}

View File

@@ -3,17 +3,19 @@ use crate::models::ids::NotificationId;
use crate::models::notifications::Notification;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(notifications_get);
cfg.service(notifications_delete);
cfg.service(notifications_read);
cfg.service(
web::scope("notification")
.service(notification_get)
.service(notifications_read)
.service(notification_delete),
);
}
@@ -31,7 +33,6 @@ pub async fn notifications_get(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
// TODO: this is really confusingly named.
use database::models::notification_item::Notification as DBNotification;
use database::models::NotificationId as DBNotificationId;
@@ -42,11 +43,8 @@ pub async fn notifications_get(
.collect();
let notifications_data: Vec<DBNotification> =
database::models::notification_item::Notification::get_many(
&notification_ids,
&**pool,
)
.await?;
database::models::notification_item::Notification::get_many(&notification_ids, &**pool)
.await?;
let notifications: Vec<Notification> = notifications_data
.into_iter()
@@ -68,11 +66,7 @@ pub async fn notification_get(
let id = info.into_inner().0;
let notification_data =
database::models::notification_item::Notification::get(
id.into(),
&**pool,
)
.await?;
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
if let Some(data) = notification_data {
if user.id == data.user_id.into() || user.role.is_admin() {
@@ -85,6 +79,39 @@ pub async fn notification_get(
}
}
#[patch("{id}")]
pub async fn notification_read(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0;
let notification_data =
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
if let Some(data) = notification_data {
if data.user_id == user.id.into() || user.role.is_admin() {
let mut transaction = pool.begin().await?;
database::models::notification_item::Notification::read(id.into(), &mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::CustomAuthentication(
"You are not authorized to read this notification!".to_string(),
))
}
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
#[delete("{id}")]
pub async fn notification_delete(
req: HttpRequest,
@@ -96,29 +123,21 @@ pub async fn notification_delete(
let id = info.into_inner().0;
let notification_data =
database::models::notification_item::Notification::get(
id.into(),
&**pool,
)
.await?;
database::models::notification_item::Notification::get(id.into(), &**pool).await?;
if let Some(data) = notification_data {
if data.user_id == user.id.into() || user.role.is_admin() {
let mut transaction = pool.begin().await?;
database::models::notification_item::Notification::remove(
id.into(),
&mut transaction,
)
.await?;
database::models::notification_item::Notification::remove(id.into(), &mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::CustomAuthentication(
"You are not authorized to delete this notification!"
.to_string(),
"You are not authorized to delete this notification!".to_string(),
))
}
} else {
@@ -126,6 +145,41 @@ pub async fn notification_delete(
}
}
#[patch("notifications")]
pub async fn notifications_read(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect::<Vec<_>>();
let mut transaction = pool.begin().await?;
let notifications_data =
database::models::notification_item::Notification::get_many(&notification_ids, &**pool)
.await?;
let mut notifications: Vec<database::models::ids::NotificationId> = Vec::new();
for notification in notifications_data {
if notification.user_id == user.id.into() || user.role.is_admin() {
notifications.push(notification.id);
}
}
database::models::notification_item::Notification::read_many(&notifications, &mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}
#[delete("notifications")]
pub async fn notifications_delete(
req: HttpRequest,
@@ -134,23 +188,18 @@ pub async fn notifications_delete(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let notification_ids =
serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect::<Vec<_>>();
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect::<Vec<_>>();
let mut transaction = pool.begin().await?;
let notifications_data =
database::models::notification_item::Notification::get_many(
&notification_ids,
&**pool,
)
.await?;
database::models::notification_item::Notification::get_many(&notification_ids, &**pool)
.await?;
let mut notifications: Vec<database::models::ids::NotificationId> =
Vec::new();
let mut notifications: Vec<database::models::ids::NotificationId> = Vec::new();
for notification in notifications_data {
if notification.user_id == user.id.into() || user.role.is_admin() {

View File

@@ -4,8 +4,8 @@ use crate::database::models::thread_item::ThreadBuilder;
use crate::file_hosting::{FileHost, FileHostingError};
use crate::models::error::ApiError;
use crate::models::projects::{
DonationLink, License, MonetizationStatus, ProjectId, ProjectStatus,
SideType, VersionId, VersionStatus,
DonationLink, License, MonetizationStatus, ProjectId, ProjectStatus, SideType, VersionId,
VersionStatus,
};
use crate::models::threads::ThreadType;
use crate::models::users::UserId;
@@ -79,14 +79,10 @@ impl actix_web::ResponseError for CreateError {
fn status_code(&self) -> StatusCode {
match self {
CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::SqlxDatabaseError(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
CreateError::SqlxDatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::IndexingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::FileHostingError(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
CreateError::FileHostingError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::SerDeError(..) => StatusCode::BAD_REQUEST,
CreateError::MultipartError(..) => StatusCode::BAD_REQUEST,
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
@@ -97,9 +93,7 @@ impl actix_web::ResponseError for CreateError {
CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST,
CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST,
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
CreateError::CustomAuthenticationError(..) => {
StatusCode::UNAUTHORIZED
}
CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED,
CreateError::SlugCollision => StatusCode::BAD_REQUEST,
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
@@ -347,21 +341,17 @@ async fn project_create_inner(
let cdn_url = dotenvy::var("CDN_URL")?;
// The currently logged in user
let current_user =
get_user_from_headers(req.headers(), &mut *transaction).await?;
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
let project_id: ProjectId =
models::generate_project_id(transaction).await?.into();
let project_id: ProjectId = models::generate_project_id(transaction).await?.into();
let project_create_data;
let mut versions;
let mut versions_map = std::collections::HashMap::new();
let mut gallery_urls = Vec::new();
let all_game_versions =
models::categories::GameVersion::list(&mut *transaction).await?;
let all_loaders =
models::categories::Loader::list(&mut *transaction).await?;
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
let all_loaders = models::categories::Loader::list(&mut *transaction).await?;
{
// The first multipart field must be named "data" and contain a
@@ -378,9 +368,9 @@ async fn project_create_inner(
})?;
let content_disposition = field.content_disposition();
let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError(String::from("Missing content name"))
})?;
let name = content_disposition
.get_name()
.ok_or_else(|| CreateError::MissingValueError(String::from("Missing content name")))?;
if name != "data" {
return Err(CreateError::InvalidInput(String::from(
@@ -390,22 +380,19 @@ async fn project_create_inner(
let mut data = Vec::new();
while let Some(chunk) = field.next().await {
data.extend_from_slice(
&chunk.map_err(CreateError::MultipartError)?,
);
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
}
let create_data: ProjectCreateData = serde_json::from_slice(&data)?;
create_data.validate().map_err(|err| {
CreateError::InvalidInput(validation_errors_to_string(err, None))
})?;
create_data
.validate()
.map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?;
let slug_project_id_option: Option<ProjectId> =
serde_json::from_str(&format!("\"{}\"", create_data.slug)).ok();
if let Some(slug_project_id) = slug_project_id_option {
let slug_project_id: models::ids::ProjectId =
slug_project_id.into();
let slug_project_id: models::ids::ProjectId = slug_project_id.into();
let results = sqlx::query!(
"
SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)
@@ -492,9 +479,7 @@ async fn project_create_inner(
let content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError(
"Missing content name".to_string(),
)
CreateError::MissingValueError("Missing content name".to_string())
})?;
let (file_name, file_extension) =
@@ -528,9 +513,7 @@ async fn project_create_inner(
)));
}
if let Some(item) =
gallery_items.iter().find(|x| x.item == name)
{
if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
let data = read_from_field(
&mut field,
5 * (1 << 20),
@@ -540,22 +523,13 @@ async fn project_create_inner(
let hash = sha1::Sha1::from(&data).hexdigest();
let (_, file_extension) =
super::version_creation::get_name_ext(
&content_disposition,
)?;
let content_type =
crate::util::ext::get_image_content_type(
file_extension,
)
super::version_creation::get_name_ext(&content_disposition)?;
let content_type = crate::util::ext::get_image_content_type(file_extension)
.ok_or_else(|| {
CreateError::InvalidIconFormat(
file_extension.to_string(),
)
CreateError::InvalidIconFormat(file_extension.to_string())
})?;
let url = format!(
"data/{project_id}/images/{hash}.{file_extension}"
);
let url = format!("data/{project_id}/images/{hash}.{file_extension}");
let upload_data = file_host
.upload_file(content_type, &url, data.freeze())
.await?;
@@ -588,8 +562,7 @@ async fn project_create_inner(
// `index` is always valid for these lists
let created_version = versions.get_mut(index).unwrap();
let version_data =
project_create_data.initial_versions.get(index).unwrap();
let version_data = project_create_data.initial_versions.get(index).unwrap();
// Upload the new jar file
super::version_creation::upload_file(
@@ -642,8 +615,7 @@ async fn project_create_inner(
}
// Convert the list of category names to actual categories
let mut categories =
Vec::with_capacity(project_create_data.categories.len());
let mut categories = Vec::with_capacity(project_create_data.categories.len());
for category in &project_create_data.categories {
let id = models::categories::Category::get_id_project(
category,
@@ -706,9 +678,7 @@ async fn project_create_inner(
)
.await?
.ok_or_else(|| {
CreateError::InvalidInput(
"Client side type specified does not exist.".to_string(),
)
CreateError::InvalidInput("Client side type specified does not exist.".to_string())
})?;
let server_side_id = models::categories::SideType::get_id(
@@ -717,35 +687,27 @@ async fn project_create_inner(
)
.await?
.ok_or_else(|| {
CreateError::InvalidInput(
"Server side type specified does not exist.".to_string(),
)
CreateError::InvalidInput("Server side type specified does not exist.".to_string())
})?;
let license_id = spdx::Expression::parse(
&project_create_data.license_id,
)
.map_err(|err| {
CreateError::InvalidInput(format!(
"Invalid SPDX license identifier: {err}"
))
})?;
let license_id =
spdx::Expression::parse(&project_create_data.license_id).map_err(|err| {
CreateError::InvalidInput(format!("Invalid SPDX license identifier: {err}"))
})?;
let mut donation_urls = vec![];
if let Some(urls) = &project_create_data.donation_urls {
for url in urls {
let platform_id = models::categories::DonationPlatform::get_id(
&url.id,
&mut *transaction,
)
.await?
.ok_or_else(|| {
CreateError::InvalidInput(format!(
"Donation platform {} does not exist.",
url.id.clone()
))
})?;
let platform_id =
models::categories::DonationPlatform::get_id(&url.id, &mut *transaction)
.await?
.ok_or_else(|| {
CreateError::InvalidInput(format!(
"Donation platform {} does not exist.",
url.id.clone()
))
})?;
donation_urls.push(models::project_item::DonationUrl {
platform_id,
@@ -856,16 +818,10 @@ async fn project_create_inner(
let _project_id = project_builder.insert(&mut *transaction).await?;
if status == ProjectStatus::Processing {
if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK")
{
crate::util::webhook::send_discord_webhook(
response.id,
pool,
webhook_url,
None,
)
.await
.ok();
if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK") {
crate::util::webhook::send_discord_webhook(response.id, pool, webhook_url, None)
.await
.ok();
}
}
@@ -888,13 +844,12 @@ async fn create_initial_version(
)));
}
version_data.validate().map_err(|err| {
CreateError::ValidationError(validation_errors_to_string(err, None))
})?;
version_data
.validate()
.map_err(|err| CreateError::ValidationError(validation_errors_to_string(err, None)))?;
// Randomly generate a new id to be used for the version
let version_id: VersionId =
models::generate_version_id(transaction).await?.into();
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
let game_versions = version_data
.game_versions
@@ -963,15 +918,8 @@ async fn process_icon_upload(
mut field: Field,
cdn_url: &str,
) -> Result<(String, Option<u32>), CreateError> {
if let Some(content_type) =
crate::util::ext::get_image_content_type(file_extension)
{
let data = read_from_field(
&mut field,
262144,
"Icons must be smaller than 256KiB",
)
.await?;
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
let color = crate::util::img::get_color_from_img(&data)?;

View File

@@ -6,16 +6,13 @@ use crate::models;
use crate::models::ids::base62_impl::parse_base62;
use crate::models::notifications::NotificationBody;
use crate::models::projects::{
DonationLink, MonetizationStatus, Project, ProjectId, ProjectStatus,
SearchRequest, SideType,
DonationLink, MonetizationStatus, Project, ProjectId, ProjectStatus, SearchRequest, SideType,
};
use crate::models::teams::Permissions;
use crate::models::threads::MessageBody;
use crate::routes::ApiError;
use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::{
filter_authorized_projects, get_user_from_headers, is_authorized,
};
use crate::util::auth::{filter_authorized_projects, get_user_from_headers, is_authorized};
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
@@ -78,30 +75,30 @@ pub async fn random_projects_get(
web::Query(count): web::Query<RandomProjects>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
count.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;
count
.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let project_ids = sqlx::query!(
"
"
SELECT id FROM mods TABLESAMPLE SYSTEM_ROWS($1) WHERE status = ANY($2)
",
count.count as i32,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_searchable()).map(|x| x.to_string()).collect::<Vec<String>>(),
)
.fetch_many(&**pool)
.try_filter_map(|e| async {
Ok(e.right().map(|m| database::models::ids::ProjectId(m.id)))
})
.try_collect::<Vec<_>>()
.await?;
count.count as i32,
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_searchable())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_many(&**pool)
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ids::ProjectId(m.id))) })
.try_collect::<Vec<_>>()
.await?;
let projects_data =
database::models::Project::get_many_full(&project_ids, &**pool)
.await?
.into_iter()
.map(Project::from)
.collect::<Vec<_>>();
let projects_data = database::models::Project::get_many_full(&project_ids, &**pool)
.await?
.into_iter()
.map(Project::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(projects_data))
}
@@ -123,13 +120,11 @@ pub async fn projects_get(
.map(|x| x.into())
.collect();
let projects_data =
database::models::Project::get_many_full(&project_ids, &**pool).await?;
let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let projects =
filter_authorized_projects(projects_data, &user_option, &pool).await?;
let projects = filter_authorized_projects(projects_data, &user_option, &pool).await?;
Ok(HttpResponse::Ok().json(projects))
}
@@ -143,10 +138,7 @@ pub async fn project_get(
let string = info.into_inner().0;
let project_data =
database::models::Project::get_full_from_slug_or_project_id(
&string, &**pool,
)
.await?;
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();
@@ -229,10 +221,7 @@ pub async fn dependency_list(
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -259,12 +248,13 @@ pub async fn dependency_list(
.try_filter_map(|e| async {
Ok(e.right().map(|x| {
(
x.dependency_id
.map(database::models::VersionId),
if x.mod_id == Some(0) { None } else { x.mod_id
.map(database::models::ProjectId) },
x.mod_dependency_id
.map(database::models::ProjectId),
x.dependency_id.map(database::models::VersionId),
if x.mod_id == Some(0) {
None
} else {
x.mod_id.map(database::models::ProjectId)
},
x.mod_dependency_id.map(database::models::ProjectId),
)
}))
})
@@ -430,15 +420,13 @@ pub async fn project_edit(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_project.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;
new_project
.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
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?;
if let Some(project_item) = result {
let id = project_item.inner.id;
@@ -456,8 +444,7 @@ pub async fn project_edit(
} else if let Some(ref member) = team_member {
permissions = Some(member.permissions)
} else if user.role.is_mod() {
permissions =
Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
permissions = Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
} else {
permissions = None
}
@@ -518,12 +505,10 @@ pub async fn project_edit(
if !(user.role.is_mod()
|| !project_item.inner.status.is_approved()
&& status == &ProjectStatus::Processing
|| project_item.inner.status.is_approved()
&& status.can_be_requested())
|| project_item.inner.status.is_approved() && status.can_be_requested())
{
return Err(ApiError::CustomAuthentication(
"You don't have permission to set this status!"
.to_string(),
"You don't have permission to set this status!".to_string(),
));
}
@@ -545,9 +530,7 @@ pub async fn project_edit(
.execute(&mut *transaction)
.await?;
if let Ok(webhook_url) =
dotenvy::var("MODERATION_DISCORD_WEBHOOK")
{
if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK") {
crate::util::webhook::send_discord_webhook(
project_item.inner.id.into(),
&pool,
@@ -559,9 +542,7 @@ pub async fn project_edit(
}
}
if status.is_approved()
&& !project_item.inner.status.is_approved()
{
if status.is_approved() && !project_item.inner.status.is_approved() {
sqlx::query!(
"
UPDATE mods
@@ -575,9 +556,7 @@ pub async fn project_edit(
}
if status.is_searchable() && !project_item.inner.webhook_sent {
if let Ok(webhook_url) =
dotenvy::var("PUBLIC_DISCORD_WEBHOOK")
{
if let Ok(webhook_url) = dotenvy::var("PUBLIC_DISCORD_WEBHOOK") {
crate::util::webhook::send_discord_webhook(
project_item.inner.id.into(),
&pool,
@@ -607,8 +586,7 @@ pub async fn project_edit(
FROM team_members tm
WHERE tm.team_id = $1 AND tm.accepted
",
project_item.inner.team_id
as database::models::ids::TeamId
project_item.inner.team_id as database::models::ids::TeamId
)
.fetch_many(&mut *transaction)
.try_filter_map(|e| async {
@@ -653,9 +631,7 @@ pub async fn project_edit(
.execute(&mut *transaction)
.await?;
if project_item.inner.status.is_searchable()
&& !status.is_searchable()
{
if project_item.inner.status.is_searchable() && !status.is_searchable() {
delete_from_index(id.into(), config).await?;
}
}
@@ -726,17 +702,14 @@ pub async fn project_edit(
for category in categories {
let category_id =
database::models::categories::Category::get_id(
category,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Category {} does not exist.",
category.clone()
))
})?;
database::models::categories::Category::get_id(category, &mut *transaction)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Category {} does not exist.",
category.clone()
))
})?;
sqlx::query!(
"
@@ -761,17 +734,14 @@ pub async fn project_edit(
for category in categories {
let category_id =
database::models::categories::Category::get_id(
category,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Category {} does not exist.",
category.clone()
))
})?;
database::models::categories::Category::get_id(category, &mut *transaction)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Category {} does not exist.",
category.clone()
))
})?;
sqlx::query!(
"
@@ -899,8 +869,7 @@ pub async fn project_edit(
));
}
let slug_project_id_option: Option<u64> =
parse_base62(slug).ok();
let slug_project_id_option: Option<u64> = parse_base62(slug).ok();
if let Some(slug_project_id) = slug_project_id_option {
let results = sqlx::query!(
"
@@ -913,8 +882,7 @@ pub async fn project_edit(
if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput(
"Slug collides with other project's id!"
.to_string(),
"Slug collides with other project's id!".to_string(),
));
}
}
@@ -933,8 +901,7 @@ pub async fn project_edit(
if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput(
"Slug collides with other project's id!"
.to_string(),
"Slug collides with other project's id!".to_string(),
));
}
}
@@ -960,13 +927,12 @@ pub async fn project_edit(
));
}
let side_type_id =
database::models::categories::SideType::get_id(
new_side.as_str(),
&mut *transaction,
)
.await?
.expect("No database entry found for side type");
let side_type_id = database::models::categories::SideType::get_id(
new_side.as_str(),
&mut *transaction,
)
.await?
.expect("No database entry found for side type");
sqlx::query!(
"
@@ -989,13 +955,12 @@ pub async fn project_edit(
));
}
let side_type_id =
database::models::categories::SideType::get_id(
new_side.as_str(),
&mut *transaction,
)
.await?
.expect("No database entry found for side type");
let side_type_id = database::models::categories::SideType::get_id(
new_side.as_str(),
&mut *transaction,
)
.await?
.expect("No database entry found for side type");
sqlx::query!(
"
@@ -1025,9 +990,7 @@ pub async fn project_edit(
}
spdx::Expression::parse(&license).map_err(|err| {
ApiError::InvalidInput(format!(
"Invalid SPDX license identifier: {err}"
))
ApiError::InvalidInput(format!("Invalid SPDX license identifier: {err}"))
})?;
sqlx::query!(
@@ -1062,18 +1025,17 @@ pub async fn project_edit(
.await?;
for donation in donations {
let platform_id =
database::models::categories::DonationPlatform::get_id(
&donation.id,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Platform {} does not exist.",
donation.id.clone()
))
})?;
let platform_id = database::models::categories::DonationPlatform::get_id(
&donation.id,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Platform {} does not exist.",
donation.id.clone()
))
})?;
sqlx::query!(
"
@@ -1090,9 +1052,7 @@ pub async fn project_edit(
}
if let Some(moderation_message) = &new_project.moderation_message {
if !user.role.is_mod()
&& project_item.inner.status != ProjectStatus::Approved
{
if !user.role.is_mod() && project_item.inner.status != ProjectStatus::Approved {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the moderation message of this project!"
.to_string(),
@@ -1112,12 +1072,8 @@ pub async fn project_edit(
.await?;
}
if let Some(moderation_message_body) =
&new_project.moderation_message_body
{
if !user.role.is_mod()
&& project_item.inner.status != ProjectStatus::Approved
{
if let Some(moderation_message_body) = &new_project.moderation_message_body {
if !user.role.is_mod() && project_item.inner.status != ProjectStatus::Approved {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the moderation message body of this project!"
.to_string(),
@@ -1158,8 +1114,7 @@ pub async fn project_edit(
.await?;
}
if let Some(monetization_status) = &new_project.monetization_status
{
if let Some(monetization_status) = &new_project.monetization_status {
if !perms.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the monetization status of this project!"
@@ -1167,8 +1122,7 @@ pub async fn project_edit(
));
}
if (*monetization_status
== MonetizationStatus::ForceDemonetized
if (*monetization_status == MonetizationStatus::ForceDemonetized
|| project_item.inner.monetization_status
== MonetizationStatus::ForceDemonetized)
&& !user.role.is_mod()
@@ -1276,9 +1230,9 @@ pub async fn projects_edit(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
bulk_edit_project.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;
bulk_edit_project
.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let project_ids: Vec<database::models::ids::ProjectId> =
serde_json::from_str::<Vec<ProjectId>>(&ids.ids)?
@@ -1286,8 +1240,7 @@ pub async fn projects_edit(
.map(|x| x.into())
.collect();
let projects_data =
database::models::Project::get_many_full(&project_ids, &**pool).await?;
let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
if let Some(id) = project_ids
.iter()
@@ -1303,15 +1256,11 @@ pub async fn projects_edit(
.iter()
.map(|x| x.inner.team_id)
.collect::<Vec<database::models::TeamId>>();
let team_members = database::models::TeamMember::get_from_team_full_many(
&team_ids, &**pool,
)
.await?;
let team_members =
database::models::TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
let categories =
database::models::categories::Category::list(&**pool).await?;
let donation_platforms =
database::models::categories::DonationPlatform::list(&**pool).await?;
let categories = database::models::categories::Category::list(&**pool).await?;
let donation_platforms = database::models::categories::DonationPlatform::list(&**pool).await?;
let mut transaction = pool.begin().await?;
@@ -1322,9 +1271,10 @@ pub async fn projects_edit(
.find(|x| x.team_id == project.inner.team_id)
{
if !member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
format!("You do not have the permissions to bulk edit project {}!", project.inner.title),
));
return Err(ApiError::CustomAuthentication(format!(
"You do not have the permissions to bulk edit project {}!",
project.inner.title
)));
}
} else if project.inner.status.is_hidden() {
return Ok(HttpResponse::NotFound().body(""));
@@ -1336,18 +1286,15 @@ pub async fn projects_edit(
};
}
let mut set_categories =
if let Some(categories) = bulk_edit_project.categories.clone() {
categories
} else {
project.categories.clone()
};
let mut set_categories = if let Some(categories) = bulk_edit_project.categories.clone() {
categories
} else {
project.categories.clone()
};
if let Some(delete_categories) = &bulk_edit_project.remove_categories {
for category in delete_categories {
if let Some(pos) =
set_categories.iter().position(|x| x == category)
{
if let Some(pos) = set_categories.iter().position(|x| x == category) {
set_categories.remove(pos);
}
}
@@ -1394,34 +1341,27 @@ pub async fn projects_edit(
project.inner.id as database::models::ids::ProjectId,
category_id as database::models::ids::CategoryId,
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
}
}
let mut set_additional_categories = if let Some(categories) =
bulk_edit_project.additional_categories.clone()
{
categories
} else {
project.additional_categories.clone()
};
let mut set_additional_categories =
if let Some(categories) = bulk_edit_project.additional_categories.clone() {
categories
} else {
project.additional_categories.clone()
};
if let Some(delete_categories) =
&bulk_edit_project.remove_additional_categories
{
if let Some(delete_categories) = &bulk_edit_project.remove_additional_categories {
for category in delete_categories {
if let Some(pos) =
set_additional_categories.iter().position(|x| x == category)
{
if let Some(pos) = set_additional_categories.iter().position(|x| x == category) {
set_additional_categories.remove(pos);
}
}
}
if let Some(add_categories) =
&bulk_edit_project.add_additional_categories
{
if let Some(add_categories) = &bulk_edit_project.add_additional_categories {
for category in add_categories {
if set_additional_categories.len() < 256 {
set_additional_categories.push(category.clone());
@@ -1476,16 +1416,14 @@ pub async fn projects_edit(
url: d.url,
})
.collect();
let mut set_donation_links = if let Some(donation_links) =
bulk_edit_project.donation_urls.clone()
{
donation_links
} else {
project_donations.clone()
};
let mut set_donation_links =
if let Some(donation_links) = bulk_edit_project.donation_urls.clone() {
donation_links
} else {
project_donations.clone()
};
if let Some(delete_donations) = &bulk_edit_project.remove_donation_urls
{
if let Some(delete_donations) = &bulk_edit_project.remove_donation_urls {
for donation in delete_donations {
if let Some(pos) = set_donation_links
.iter()
@@ -1532,8 +1470,8 @@ pub async fn projects_edit(
platform_id as database::models::ids::DonationPlatformId,
donation.url
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
}
}
@@ -1616,8 +1554,7 @@ pub async fn project_schedule(
if scheduling_data.time < Utc::now() {
return Err(ApiError::InvalidInput(
"You cannot schedule a project to be released in the past!"
.to_string(),
"You cannot schedule a project to be released in the past!".to_string(),
));
}
@@ -1628,10 +1565,7 @@ pub async fn project_schedule(
}
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
if let Some(project_item) = result {
let team_member = database::models::TeamMember::get_from_user_id(
@@ -1684,22 +1618,15 @@ pub async fn project_icon_edit(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) =
crate::util::ext::get_image_content_type(&ext.ext)
{
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
let project_item =
database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !user.role.is_mod() {
@@ -1711,15 +1638,12 @@ pub async fn project_icon_edit(
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's icon."
.to_string(),
"You don't have permission to edit this project's icon.".to_string(),
));
}
}
@@ -1732,12 +1656,8 @@ pub async fn project_icon_edit(
}
}
let bytes = read_from_payload(
&mut payload,
262144,
"Icons must be smaller than 256KiB",
)
.await?;
let bytes =
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
let color = crate::util::img::get_color_from_img(&bytes)?;
@@ -1787,15 +1707,11 @@ pub async fn delete_project_icon(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
let project_item = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
@@ -1806,15 +1722,12 @@ pub async fn delete_project_icon(
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's icon."
.to_string(),
"You don't have permission to edit this project's icon.".to_string(),
));
}
}
@@ -1866,32 +1779,24 @@ pub async fn add_gallery_item(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) =
crate::util::ext::get_image_content_type(&ext.ext)
{
item.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
item.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
let project_item =
database::models::Project::get_full_from_slug_or_project_id(
&string, &**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if project_item.gallery_items.len() > 64 {
return Err(ApiError::CustomAuthentication(
"You have reached the maximum of gallery images to upload."
.to_string(),
"You have reached the maximum of gallery images to upload.".to_string(),
));
}
@@ -1904,15 +1809,12 @@ pub async fn add_gallery_item(
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery."
.to_string(),
"You don't have permission to edit this project's gallery.".to_string(),
));
}
}
@@ -2013,19 +1915,14 @@ pub async fn edit_gallery_item(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
item.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;
item.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let project_item = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
@@ -2036,15 +1933,12 @@ pub async fn edit_gallery_item(
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery."
.to_string(),
"You don't have permission to edit this project's gallery.".to_string(),
));
}
}
@@ -2157,15 +2051,11 @@ pub async fn delete_gallery_item(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
let project_item = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
@@ -2176,15 +2066,12 @@ pub async fn delete_gallery_item(
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this project's gallery."
.to_string(),
"You don't have permission to edit this project's gallery.".to_string(),
));
}
}
@@ -2241,30 +2128,23 @@ pub async fn project_delete(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
let project = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let project = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !user.role.is_admin() {
let team_member =
database::models::TeamMember::get_from_user_id_project(
project.id,
user.id.into(),
&**pool,
)
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let team_member = database::models::TeamMember::get_from_user_id_project(
project.id,
user.id.into(),
&**pool,
)
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if !team_member
.permissions
@@ -2278,9 +2158,7 @@ pub async fn project_delete(
let mut transaction = pool.begin().await?;
let result =
database::models::Project::remove_full(project.id, &mut transaction)
.await?;
let result = database::models::Project::remove_full(project.id, &mut transaction).await?;
transaction.commit().await?;
@@ -2302,15 +2180,11 @@ pub async fn project_follow(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
let user_id: database::models::ids::UserId = user.id.into();
let project_id: database::models::ids::ProjectId = result.id;
@@ -2375,15 +2249,11 @@ pub async fn project_unfollow(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The specified project does not exist!".to_string(),
)
})?;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
let user_id: database::models::ids::UserId = user.id.into();
let project_id = result.id;
@@ -2439,8 +2309,7 @@ pub async fn delete_from_index(
id: ProjectId,
config: web::Data<SearchConfig>,
) -> Result<(), meilisearch_sdk::errors::Error> {
let client =
meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
let indexes: IndexesResults = client.get_indexes().await?;

View File

@@ -1,15 +1,9 @@
use crate::database::models::thread_item::{
ThreadBuilder, ThreadMessageBuilder,
};
use crate::models::ids::{
base62_impl::parse_base62, ProjectId, UserId, VersionId,
};
use crate::database::models::thread_item::{ThreadBuilder, ThreadMessageBuilder};
use crate::models::ids::{base62_impl::parse_base62, ProjectId, UserId, VersionId};
use crate::models::reports::{ItemType, Report};
use crate::models::threads::{MessageBody, ThreadType};
use crate::routes::ApiError;
use crate::util::auth::{
check_is_moderator_from_headers, get_user_from_headers,
};
use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::StreamExt;
@@ -41,31 +35,24 @@ pub async fn report_create(
) -> Result<HttpResponse, ApiError> {
let mut transaction = pool.begin().await?;
let current_user =
get_user_from_headers(req.headers(), &mut *transaction).await?;
let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?;
let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await {
bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInput(
"Error while parsing request payload!".to_string(),
)
ApiError::InvalidInput("Error while parsing request payload!".to_string())
})?);
}
let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?;
let id =
crate::database::models::generate_report_id(&mut transaction).await?;
let id = crate::database::models::generate_report_id(&mut transaction).await?;
let report_type = crate::database::models::categories::ReportType::get_id(
&new_report.report_type,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Invalid report type: {}",
new_report.report_type
))
ApiError::InvalidInput(format!("Invalid report type: {}", new_report.report_type))
})?;
let thread_id = ThreadBuilder {
@@ -92,8 +79,7 @@ pub async fn report_create(
match new_report.item_type {
ItemType::Project => {
let project_id =
ProjectId(parse_base62(new_report.item_id.as_str())?);
let project_id = ProjectId(parse_base62(new_report.item_id.as_str())?);
let result = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)",
@@ -112,8 +98,7 @@ pub async fn report_create(
report.project_id = Some(project_id.into())
}
ItemType::Version => {
let version_id =
VersionId(parse_base62(new_report.item_id.as_str())?);
let version_id = VersionId(parse_base62(new_report.item_id.as_str())?);
let result = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)",
@@ -236,11 +221,8 @@ pub async fn reports(
.await?
};
let query_reports = crate::database::models::report_item::Report::get_many(
&report_ids,
&**pool,
)
.await?;
let query_reports =
crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?;
let mut reports = Vec::new();
@@ -260,8 +242,7 @@ pub async fn report_get(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0.into();
let report =
crate::database::models::report_item::Report::get(id, &**pool).await?;
let report = crate::database::models::report_item::Report::get(id, &**pool).await?;
if let Some(report) = report {
if !user.role.is_mod() && report.reporter != user.id.into() {
@@ -291,8 +272,7 @@ pub async fn report_edit(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id = info.into_inner().0.into();
let report =
crate::database::models::report_item::Report::get(id, &**pool).await?;
let report = crate::database::models::report_item::Report::get(id, &**pool).await?;
if let Some(report) = report {
if !user.role.is_mod() && report.user_id != Some(user.id.into()) {
@@ -380,9 +360,7 @@ pub async fn report_delete(
}
}
fn to_report(
x: crate::database::models::report_item::QueryReport,
) -> Result<Report, ApiError> {
fn to_report(x: crate::database::models::report_item::QueryReport) -> Result<Report, ApiError> {
let mut item_id = "".to_string();
let mut item_type = ItemType::Unknown;

View File

@@ -8,9 +8,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
}
#[get("statistics")]
pub async fn get_stats(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let projects = sqlx::query!(
"
SELECT COUNT(id)

View File

@@ -1,8 +1,6 @@
use super::ApiError;
use crate::database::models;
use crate::database::models::categories::{
DonationPlatform, ProjectType, ReportType, SideType,
};
use crate::database::models::categories::{DonationPlatform, ProjectType, ReportType, SideType};
use actix_web::{get, web, HttpResponse};
use chrono::{DateTime, Utc};
use models::categories::{Category, GameVersion, Loader};
@@ -34,9 +32,7 @@ pub struct CategoryData {
// TODO: searching / filtering? Could be used to implement a live
// searching category list
#[get("category")]
pub async fn category_list(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
pub async fn category_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let results = Category::list(&**pool)
.await?
.into_iter()
@@ -59,9 +55,7 @@ pub struct LoaderData {
}
#[get("loader")]
pub async fn loader_list(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
pub async fn loader_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let mut results = Loader::list(&**pool)
.await?
.into_iter()
@@ -97,11 +91,8 @@ pub async fn game_version_list(
pool: web::Data<PgPool>,
query: web::Query<GameVersionQuery>,
) -> Result<HttpResponse, ApiError> {
let results: Vec<GameVersionQueryData> = if query.type_.is_some()
|| query.major.is_some()
{
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool)
.await?
let results: Vec<GameVersionQueryData> = if query.type_.is_some() || query.major.is_some() {
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?
} else {
GameVersion::list(&**pool).await?
}
@@ -145,9 +136,7 @@ pub struct LicenseText {
}
#[get("license/{id}")]
pub async fn license_text(
params: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
pub async fn license_text(params: web::Path<(String,)>) -> Result<HttpResponse, ApiError> {
let license_id = params.into_inner().0;
if license_id == *crate::models::projects::DEFAULT_LICENSE_ID {
@@ -176,41 +165,32 @@ pub struct DonationPlatformQueryData {
}
#[get("donation_platform")]
pub async fn donation_platform_list(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let results: Vec<DonationPlatformQueryData> =
DonationPlatform::list(&**pool)
.await?
.into_iter()
.map(|x| DonationPlatformQueryData {
short: x.short,
name: x.name,
})
.collect();
pub async fn donation_platform_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let results: Vec<DonationPlatformQueryData> = DonationPlatform::list(&**pool)
.await?
.into_iter()
.map(|x| DonationPlatformQueryData {
short: x.short,
name: x.name,
})
.collect();
Ok(HttpResponse::Ok().json(results))
}
#[get("report_type")]
pub async fn report_type_list(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
pub async fn report_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let results = ReportType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results))
}
#[get("project_type")]
pub async fn project_type_list(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
pub async fn project_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let results = ProjectType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results))
}
#[get("side_type")]
pub async fn side_type_list(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
pub async fn side_type_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
let results = SideType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results))
}

View File

@@ -33,33 +33,23 @@ pub async fn team_members_get_project(
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let project_data =
crate::database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?;
crate::database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
if let Some(project) = project_data {
let members_data =
TeamMember::get_from_team_full(project.team_id, &**pool).await?;
let members_data = TeamMember::get_from_team_full(project.team_id, &**pool).await?;
let current_user =
get_user_from_headers(req.headers(), &**pool).await.ok();
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(user) = current_user {
let team_member = TeamMember::get_from_user_id(
project.team_id,
user.id.into(),
&**pool,
)
.await
.map_err(ApiError::Database)?;
let team_member =
TeamMember::get_from_user_id(project.team_id, user.id.into(), &**pool)
.await
.map_err(ApiError::Database)?;
if team_member.is_some() {
let team_members: Vec<_> = members_data
.into_iter()
.map(|data| {
crate::models::teams::TeamMember::from(data, false)
})
.map(|data| crate::models::teams::TeamMember::from(data, false))
.collect();
return Ok(HttpResponse::Ok().json(team_members));
@@ -85,16 +75,14 @@ pub async fn team_members_get(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let members_data =
TeamMember::get_from_team_full(id.into(), &**pool).await?;
let members_data = TeamMember::get_from_team_full(id.into(), &**pool).await?;
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(user) = &current_user {
let team_member =
TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
.await
.map_err(ApiError::Database)?;
let team_member = TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
.await
.map_err(ApiError::Database)?;
if team_member.is_some() {
let team_members: Vec<_> = members_data
@@ -139,8 +127,7 @@ pub async fn teams_get(
.map(|x| x.into())
.collect::<Vec<crate::database::models::ids::TeamId>>();
let teams_data =
TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
let accepted = if let Some(user) = current_user {
@@ -159,9 +146,8 @@ pub async fn teams_get(
for (id, member_data) in &teams_groups {
if accepted.contains(&id) {
let team_members = member_data.map(|data| {
crate::models::teams::TeamMember::from(data, false)
});
let team_members =
member_data.map(|data| crate::models::teams::TeamMember::from(data, false));
teams.push(team_members.collect());
@@ -187,12 +173,8 @@ pub async fn join_team(
let team_id = info.into_inner().0.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = TeamMember::get_from_user_id_pending(
team_id,
current_user.id.into(),
&**pool,
)
.await?;
let member =
TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?;
if let Some(member) = member {
if member.accepted {
@@ -258,20 +240,17 @@ pub async fn add_team_member(
let mut transaction = pool.begin().await?;
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member =
TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
})?;
let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
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::CustomAuthentication(
"You don't have permission to invite users to this team"
.to_string(),
"You don't have permission to invite users to this team".to_string(),
));
}
if !member.permissions.contains(new_member.permissions) {
@@ -286,9 +265,7 @@ pub async fn add_team_member(
));
}
if new_member.payouts_split < Decimal::ZERO
|| new_member.payouts_split > Decimal::from(5000)
{
if new_member.payouts_split < Decimal::ZERO || new_member.payouts_split > Decimal::from(5000) {
return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(),
));
@@ -308,21 +285,16 @@ pub async fn add_team_member(
));
} else {
return Err(ApiError::InvalidInput(
"There is already a pending member request for this user"
.to_string(),
"There is already a pending member request for this user".to_string(),
));
}
}
crate::database::models::User::get(member.user_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("An invalid User ID specified".to_string())
})?;
.ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?;
let new_id =
crate::database::models::ids::generate_team_member_id(&mut transaction)
.await?;
let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?;
TeamMember {
id: new_id,
team_id,
@@ -383,24 +355,20 @@ pub async fn edit_team_member(
let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member =
TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
})?;
let edit_member_db =
TeamMember::get_from_user_id_pending(id, user_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
})?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
let edit_member_db = TeamMember::get_from_user_id_pending(id, user_id, &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
let mut transaction = pool.begin().await?;
@@ -408,30 +376,26 @@ pub async fn edit_team_member(
&& (edit_member.role.is_some() || edit_member.permissions.is_some())
{
return Err(ApiError::InvalidInput(
"The owner's permission and role of a team cannot be edited"
.to_string(),
"The owner's permission and role of a team cannot be edited".to_string(),
));
}
if !member.permissions.contains(Permissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
"You don't have permission to edit members of this team".to_string(),
));
}
if let Some(new_permissions) = edit_member.permissions {
if !member.permissions.contains(new_permissions) {
return Err(ApiError::InvalidInput(
"The new permissions have permissions that you don't have"
.to_string(),
"The new permissions have permissions that you don't have".to_string(),
));
}
}
if let Some(payouts_split) = edit_member.payouts_split {
if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000)
{
if payouts_split < Decimal::ZERO || payouts_split > Decimal::from(5000) {
return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(),
));
@@ -478,38 +442,26 @@ pub async fn transfer_ownership(
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
if !current_user.role.is_admin() {
let member = TeamMember::get_from_user_id(
id.into(),
current_user.id.into(),
&**pool,
)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
})?;
let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
if member.role != crate::models::teams::OWNER_ROLE {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit the ownership of this team"
.to_string(),
"You don't have permission to edit the ownership of this team".to_string(),
));
}
}
let new_member = TeamMember::get_from_user_id(
id.into(),
new_owner.user_id.into(),
&**pool,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"The new owner specified does not exist".to_string(),
)
})?;
let new_member = TeamMember::get_from_user_id(id.into(), new_owner.user_id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The new owner specified does not exist".to_string())
})?;
if !new_member.accepted {
return Err(ApiError::InvalidInput(
@@ -559,18 +511,15 @@ pub async fn remove_team_member(
let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member =
TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team"
.to_string(),
)
})?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to edit members of this team".to_string(),
)
})?;
let delete_member =
TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
let delete_member = TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
if let Some(delete_member) = delete_member {
if delete_member.role == crate::models::teams::OWNER_ROLE {
@@ -586,8 +535,7 @@ pub async fn remove_team_member(
// Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission.
if delete_member.user_id == member.user_id
|| (member.permissions.contains(Permissions::REMOVE_MEMBER)
&& member.accepted)
|| (member.permissions.contains(Permissions::REMOVE_MEMBER) && member.accepted)
{
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
@@ -596,8 +544,7 @@ pub async fn remove_team_member(
));
}
} else if delete_member.user_id == member.user_id
|| (member.permissions.contains(Permissions::MANAGE_INVITES)
&& member.accepted)
|| (member.permissions.contains(Permissions::MANAGE_INVITES) && member.accepted)
{
// This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES
@@ -605,8 +552,7 @@ pub async fn remove_team_member(
TeamMember::delete(id, user_id, &mut transaction).await?;
} else {
return Err(ApiError::CustomAuthentication(
"You do not have permission to cancel a team invite"
.to_string(),
"You do not have permission to cancel a team invite".to_string(),
));
}

View File

@@ -4,14 +4,10 @@ use crate::database::models::thread_item::ThreadMessageBuilder;
use crate::models::ids::{ReportId, ThreadMessageId};
use crate::models::notifications::NotificationBody;
use crate::models::projects::{ProjectId, ProjectStatus};
use crate::models::threads::{
MessageBody, Thread, ThreadId, ThreadMessage, ThreadType,
};
use crate::models::threads::{MessageBody, Thread, ThreadId, ThreadMessage, ThreadType};
use crate::models::users::User;
use crate::routes::ApiError;
use crate::util::auth::{
check_is_moderator_from_headers, get_user_from_headers,
};
use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use futures::TryStreamExt;
use serde::Deserialize;
@@ -42,13 +38,13 @@ pub async fn is_authorized_thread(
Ok(match thread.type_ {
ThreadType::Report => {
let report_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)",
thread.id as database::models::ids::ThreadId,
user_id as database::models::ids::UserId,
)
.fetch_one(pool)
.await?
.exists;
"SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)",
thread.id as database::models::ids::ThreadId,
user_id as database::models::ids::UserId,
)
.fetch_one(pool)
.await?
.exists;
report_exists.unwrap_or(false)
}
@@ -80,8 +76,7 @@ pub async fn filter_authorized_threads(
for thread in threads {
if user.role.is_mod()
|| (thread.type_ == ThreadType::DirectMessage
&& thread.members.contains(&user_id))
|| (thread.type_ == ThreadType::DirectMessage && thread.members.contains(&user_id))
{
return_threads.push(thread);
} else {
@@ -106,23 +101,23 @@ pub async fn filter_authorized_threads(
&*project_thread_ids,
user_id as database::models::ids::UserId,
)
.fetch_many(&***pool)
.try_for_each(|e| {
if let Some(row) = e.right() {
check_threads.retain(|x| {
let bool = Some(x.id.0) == row.thread_id;
.fetch_many(&***pool)
.try_for_each(|e| {
if let Some(row) = e.right() {
check_threads.retain(|x| {
let bool = Some(x.id.0) == row.thread_id;
if bool {
return_threads.push(x.clone());
}
if bool {
return_threads.push(x.clone());
}
!bool
});
}
!bool
});
}
futures::future::ready(Ok(()))
})
.await?;
futures::future::ready(Ok(()))
})
.await?;
}
let report_thread_ids = check_threads
@@ -176,12 +171,11 @@ pub async fn filter_authorized_threads(
.collect::<Vec<database::models::UserId>>(),
);
let users: Vec<User> =
database::models::User::get_many(&user_ids, &***pool)
.await?
.into_iter()
.map(From::from)
.collect();
let users: Vec<User> = database::models::User::get_many(&user_ids, &***pool)
.await?
.into_iter()
.map(From::from)
.collect();
let mut final_threads = Vec::new();
@@ -210,11 +204,7 @@ pub async fn filter_authorized_threads(
Ok(final_threads)
}
fn convert_thread(
data: database::models::Thread,
users: Vec<User>,
user: &User,
) -> Thread {
fn convert_thread(data: database::models::Thread, users: Vec<User>, user: &User) -> Thread {
let thread_type = data.type_;
Thread {
@@ -279,16 +269,13 @@ pub async fn thread_get(
.collect::<Vec<_>>(),
);
let users: Vec<User> =
database::models::User::get_many(authors, &**pool)
.await?
.into_iter()
.map(From::from)
.collect();
let users: Vec<User> = database::models::User::get_many(authors, &**pool)
.await?
.into_iter()
.map(From::from)
.collect();
return Ok(
HttpResponse::Ok().json(convert_thread(data, users, &user))
);
return Ok(HttpResponse::Ok().json(convert_thread(data, users, &user)));
}
}
Ok(HttpResponse::NotFound().body(""))
@@ -313,8 +300,7 @@ pub async fn threads_get(
.map(|x| x.into())
.collect();
let threads_data =
database::models::Thread::get_many(&thread_ids, &**pool).await?;
let threads_data = database::models::Thread::get_many(&thread_ids, &**pool).await?;
let threads = filter_authorized_threads(threads_data, &user, &pool).await?;
@@ -356,17 +342,13 @@ pub async fn thread_send_message(
}
if let Some(replying_to) = replying_to {
let thread_message = database::models::ThreadMessage::get(
(*replying_to).into(),
&**pool,
)
.await?;
let thread_message =
database::models::ThreadMessage::get((*replying_to).into(), &**pool).await?;
if let Some(thread_message) = thread_message {
if thread_message.thread_id != string {
return Err(ApiError::InvalidInput(
"Message replied to is from another thread!"
.to_string(),
"Message replied to is from another thread!".to_string(),
));
}
} else {
@@ -394,17 +376,13 @@ pub async fn thread_send_message(
None
};
if report.as_ref().map(|x| x.closed).unwrap_or(false)
&& !user.role.is_mod()
{
if report.as_ref().map(|x| x.closed).unwrap_or(false) && !user.role.is_mod() {
return Err(ApiError::InvalidInput(
"You may not reply to a closed report".to_string(),
));
}
let (mod_notif, (user_notif, team_id)) = if thread.type_
== ThreadType::Project
{
let (mod_notif, (user_notif, team_id)) = if thread.type_ == ThreadType::Project {
let record = sqlx::query!(
"SELECT m.status, m.team_id FROM mods m WHERE thread_id = $1",
thread.id as database::models::ids::ThreadId,
@@ -422,7 +400,17 @@ pub async fn thread_send_message(
),
)
} else {
(false, (thread.type_ == ThreadType::Report, None))
(
!user.role.is_mod(),
(
thread.type_ == ThreadType::Report
&& !report
.as_ref()
.map(|x| x.reporter == user.id.into())
.unwrap_or(false),
None,
),
)
};
let mut transaction = pool.begin().await?;
@@ -498,14 +486,11 @@ pub async fn moderation_inbox(
"
)
.fetch_many(&**pool)
.try_filter_map(|e| async {
Ok(e.right().map(|m| database::models::ThreadId(m.id)))
})
.try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ThreadId(m.id))) })
.try_collect::<Vec<database::models::ThreadId>>()
.await?;
let threads_data =
database::models::Thread::get_many(&ids, &**pool).await?;
let threads_data = database::models::Thread::get_many(&ids, &**pool).await?;
let threads = filter_authorized_threads(threads_data, &user, &pool).await?;
Ok(HttpResponse::Ok().json(threads))
@@ -546,11 +531,7 @@ pub async fn message_delete(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let result = database::models::ThreadMessage::get(
info.into_inner().0.into(),
&**pool,
)
.await?;
let result = database::models::ThreadMessage::get(info.into_inner().0.into(), &**pool).await?;
if let Some(thread) = result {
if !user.role.is_mod() && thread.author_id != Some(user.id.into()) {
@@ -560,11 +541,7 @@ pub async fn message_delete(
}
let mut transaction = pool.begin().await?;
database::models::ThreadMessage::remove_full(
thread.id,
&mut transaction,
)
.await?;
database::models::ThreadMessage::remove_full(thread.id, &mut transaction).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))

View File

@@ -2,9 +2,7 @@ use crate::database::models::User;
use crate::file_hosting::FileHost;
use crate::models::notifications::Notification;
use crate::models::projects::Project;
use crate::models::users::{
Badges, RecipientType, RecipientWallet, Role, UserId,
};
use crate::models::users::{Badges, RecipientType, RecipientWallet, Role, UserId};
use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue};
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
@@ -24,6 +22,7 @@ use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(user_auth_get);
cfg.service(user_data_get);
cfg.service(users_get);
cfg.service(
@@ -45,8 +44,44 @@ pub async fn user_auth_get(
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
Ok(HttpResponse::Ok()
.json(get_user_from_headers(req.headers(), &**pool).await?))
Ok(HttpResponse::Ok().json(get_user_from_headers(req.headers(), &**pool).await?))
}
#[derive(Serialize)]
pub struct UserData {
pub notifs_count: u64,
pub followed_projects: Vec<crate::models::ids::ProjectId>,
}
#[get("user_data")]
pub async fn user_data_get(
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let data = sqlx::query!(
"
SELECT COUNT(DISTINCT n.id) notifs_count, ARRAY_AGG(mf.mod_id) followed_projects FROM notifications n
LEFT OUTER JOIN mod_follows mf ON mf.follower_id = $1
WHERE user_id = $1 AND read = FALSE
",
user.id.0 as i64
).fetch_optional(&**pool).await?;
if let Some(data) = data {
Ok(HttpResponse::Ok().json(UserData {
notifs_count: data.notifs_count.map(|x| x as u64).unwrap_or(0),
followed_projects: data
.followed_projects
.unwrap_or_default()
.into_iter()
.map(|x| crate::models::ids::ProjectId(x as u64))
.collect(),
}))
} else {
Ok(HttpResponse::NoContent().body(""))
}
}
#[derive(Serialize, Deserialize)]
@@ -66,8 +101,7 @@ pub async fn users_get(
let users_data = User::get_many(&user_ids, &**pool).await?;
let users: Vec<crate::models::users::User> =
users_data.into_iter().map(From::from).collect();
let users: Vec<crate::models::users::User> = users_data.into_iter().map(From::from).collect();
Ok(HttpResponse::Ok().json(users))
}
@@ -78,8 +112,7 @@ pub async fn user_get(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let id_option: Option<UserId> =
serde_json::from_str(&format!("\"{string}\"")).ok();
let id_option: Option<UserId> = serde_json::from_str(&format!("\"{string}\"")).ok();
let mut user_data;
@@ -109,8 +142,7 @@ pub async fn projects_list(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await.ok();
let id_option =
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
let user_id: UserId = id.into();
@@ -121,13 +153,12 @@ pub async fn projects_list(
let project_data = User::get_projects(id, &**pool).await?;
let response: Vec<_> =
crate::database::Project::get_many_full(&project_data, &**pool)
.await?
.into_iter()
.filter(|x| can_view_private || x.inner.status.is_searchable())
.map(Project::from)
.collect();
let response: Vec<_> = crate::database::Project::get_many_full(&project_data, &**pool)
.await?
.into_iter()
.filter(|x| can_view_private || x.inner.status.is_searchable())
.map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(response))
} else {
@@ -192,12 +223,11 @@ pub async fn user_edit(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_user.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;
new_user
.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let id_option =
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
let user_id: UserId = id.into();
@@ -320,23 +350,21 @@ pub async fn user_edit(
if let Some(payout_data) = &new_user.payout_data {
if let Some(payout_data) = payout_data {
if payout_data.payout_wallet_type
== RecipientType::UserHandle
if payout_data.payout_wallet_type == RecipientType::UserHandle
&& payout_data.payout_wallet == RecipientWallet::Paypal
{
return Err(ApiError::InvalidInput(
"You cannot use a paypal wallet with a user handle!"
.to_string(),
"You cannot use a paypal wallet with a user handle!".to_string(),
));
}
if !match payout_data.payout_wallet_type {
RecipientType::Email => validator::validate_email(
&payout_data.payout_address,
),
RecipientType::Phone => validator::validate_phone(
&payout_data.payout_address,
),
RecipientType::Email => {
validator::validate_email(&payout_data.payout_address)
}
RecipientType::Phone => {
validator::validate_phone(&payout_data.payout_address)
}
RecipientType::UserHandle => true,
} {
return Err(ApiError::InvalidInput(
@@ -350,8 +378,8 @@ pub async fn user_edit(
",
id as crate::database::models::ids::UserId,
)
.fetch_one(&mut *transaction)
.await?;
.fetch_one(&mut *transaction)
.await?;
if results.exists.unwrap_or(false) {
return Err(ApiError::InvalidInput(
@@ -371,8 +399,8 @@ pub async fn user_edit(
payout_data.payout_address,
id as crate::database::models::ids::UserId,
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
} else {
sqlx::query!(
"
@@ -382,8 +410,8 @@ pub async fn user_edit(
",
id as crate::database::models::ids::UserId,
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
}
}
@@ -413,20 +441,15 @@ pub async fn user_icon_edit(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) =
crate::util::ext::get_image_content_type(&ext.ext)
{
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option =
User::get_id_from_username_or_id(&info.into_inner().0, &**pool)
.await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
if user.id != id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this user's icon."
.to_string(),
"You don't have permission to edit this user's icon.".to_string(),
));
}
@@ -452,12 +475,8 @@ pub async fn user_icon_edit(
}
}
let bytes = read_from_payload(
&mut payload,
2097152,
"Icons must be smaller than 2MiB",
)
.await?;
let bytes =
read_from_payload(&mut payload, 2097152, "Icons must be smaller than 2MiB").await?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let upload_data = file_host
@@ -509,8 +528,7 @@ pub async fn user_delete(
removal_type: web::Query<RemovalType>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option =
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() {
@@ -546,11 +564,7 @@ pub async fn user_follows(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = crate::database::models::User::get_id_from_username_or_id(
&info.into_inner().0,
&**pool,
)
.await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() {
@@ -576,12 +590,11 @@ pub async fn user_follows(
.try_collect::<Vec<crate::database::models::ProjectId>>()
.await?;
let projects: Vec<_> =
crate::database::Project::get_many_full(&project_ids, &**pool)
.await?
.into_iter()
.map(Project::from)
.collect();
let projects: Vec<_> = crate::database::Project::get_many_full(&project_ids, &**pool)
.await?
.into_iter()
.map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects))
} else {
@@ -596,11 +609,7 @@ pub async fn user_notifications(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = crate::database::models::User::get_id_from_username_or_id(
&info.into_inner().0,
&**pool,
)
.await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() {
@@ -638,14 +647,12 @@ pub async fn user_payouts(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option =
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to see the payouts of this user!"
.to_string(),
"You do not have permission to see the payouts of this user!".to_string(),
));
}
@@ -717,22 +724,18 @@ pub async fn user_payouts_request(
let mut payouts_queue = payouts_queue.lock().await;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option =
User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
if let Some(id) = id_option {
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to request payouts of this user!"
.to_string(),
"You do not have permission to request payouts of this user!".to_string(),
));
}
if let Some(payouts_data) = user.payout_data {
if let Some(payout_address) = payouts_data.payout_address {
if let Some(payout_wallet_type) =
payouts_data.payout_wallet_type
{
if let Some(payout_wallet_type) = payouts_data.payout_wallet_type {
if let Some(payout_wallet) = payouts_data.payout_wallet {
return if data.amount < payouts_data.balance {
let mut transaction = pool.begin().await?;
@@ -744,10 +747,15 @@ pub async fn user_payouts_request(
value: data.amount,
},
receiver: payout_address,
note: "Payment from Modrinth creator monetization program".to_string(),
note: "Payment from Modrinth creator monetization program"
.to_string(),
recipient_type: payout_wallet_type.to_string().to_uppercase(),
recipient_wallet: payout_wallet.as_str_api().to_string(),
sender_item_id: format!("{}-{}", UserId::from(id), Utc::now().timestamp()),
sender_item_id: format!(
"{}-{}",
UserId::from(id),
Utc::now().timestamp()
),
})
.await?;
@@ -760,8 +768,8 @@ pub async fn user_payouts_request(
data.amount,
"success"
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
sqlx::query!(
"
@@ -780,8 +788,7 @@ pub async fn user_payouts_request(
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInput(
"You do not have enough funds to make this payout!"
.to_string(),
"You do not have enough funds to make this payout!".to_string(),
))
};
}

View File

@@ -8,8 +8,8 @@ use crate::file_hosting::FileHost;
use crate::models::notifications::NotificationBody;
use crate::models::pack::PackFileHash;
use crate::models::projects::{
Dependency, DependencyType, FileType, GameVersion, Loader, ProjectId,
Version, VersionFile, VersionId, VersionStatus, VersionType,
Dependency, DependencyType, FileType, GameVersion, Loader, ProjectId, Version, VersionFile,
VersionId, VersionStatus, VersionType,
};
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers;
@@ -97,11 +97,8 @@ pub async fn version_create(
.await;
if result.is_err() {
let undo_result = super::project_creation::undo_uploads(
&***file_host,
&uploaded_files,
)
.await;
let undo_result =
super::project_creation::undo_uploads(&***file_host, &uploaded_files).await;
let rollback_result = transaction.rollback().await;
undo_result?;
@@ -127,10 +124,8 @@ async fn version_create_inner(
let mut initial_version_data = None;
let mut version_builder = None;
let all_game_versions =
models::categories::GameVersion::list(&mut *transaction).await?;
let all_loaders =
models::categories::Loader::list(&mut *transaction).await?;
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
let all_loaders = models::categories::Loader::list(&mut *transaction).await?;
let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
@@ -145,9 +140,7 @@ async fn version_create_inner(
let result = async {
let content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError(
"Missing content name".to_string(),
)
CreateError::MissingValueError("Missing content name".to_string())
})?;
if name == "data" {
@@ -156,11 +149,9 @@ async fn version_create_inner(
data.extend_from_slice(&chunk?);
}
let version_create_data: InitialVersionData =
serde_json::from_slice(&data)?;
let version_create_data: InitialVersionData = serde_json::from_slice(&data)?;
initial_version_data = Some(version_create_data);
let version_create_data =
initial_version_data.as_ref().unwrap();
let version_create_data = initial_version_data.as_ref().unwrap();
if version_create_data.project_id.is_none() {
return Err(CreateError::MissingValueError(
"Missing project id".to_string(),
@@ -168,9 +159,7 @@ async fn version_create_inner(
}
version_create_data.validate().map_err(|err| {
CreateError::ValidationError(validation_errors_to_string(
err, None,
))
CreateError::ValidationError(validation_errors_to_string(err, None))
})?;
if !version_create_data.status.can_be_requested() {
@@ -179,8 +168,7 @@ async fn version_create_inner(
));
}
let project_id: models::ProjectId =
version_create_data.project_id.unwrap().into();
let project_id: models::ProjectId = version_create_data.project_id.unwrap().into();
// Ensure that the project this version is being added to exists
let results = sqlx::query!(
@@ -206,8 +194,7 @@ async fn version_create_inner(
.await?
.ok_or_else(|| {
CreateError::CustomAuthenticationError(
"You don't have permission to upload this version!"
.to_string(),
"You don't have permission to upload this version!".to_string(),
)
})?;
@@ -216,13 +203,11 @@ async fn version_create_inner(
.contains(Permissions::UPLOAD_VERSION)
{
return Err(CreateError::CustomAuthenticationError(
"You don't have permission to upload this version!"
.to_string(),
"You don't have permission to upload this version!".to_string(),
));
}
let version_id: VersionId =
models::generate_version_id(transaction).await?.into();
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
let project_type = sqlx::query!(
"
@@ -243,13 +228,10 @@ async fn version_create_inner(
all_game_versions
.iter()
.find(|y| y.version == x.0)
.ok_or_else(|| {
CreateError::InvalidGameVersion(x.0.clone())
})
.ok_or_else(|| CreateError::InvalidGameVersion(x.0.clone()))
.map(|y| y.id)
})
.collect::<Result<Vec<models::GameVersionId>, CreateError>>(
)?;
.collect::<Result<Vec<models::GameVersionId>, CreateError>>()?;
let loaders = version_create_data
.loaders
@@ -258,13 +240,9 @@ async fn version_create_inner(
all_loaders
.iter()
.find(|y| {
y.loader == x.0
&& y.supported_project_types
.contains(&project_type)
})
.ok_or_else(|| {
CreateError::InvalidLoader(x.0.clone())
y.loader == x.0 && y.supported_project_types.contains(&project_type)
})
.ok_or_else(|| CreateError::InvalidLoader(x.0.clone()))
.map(|y| y.id)
})
.collect::<Result<Vec<models::LoaderId>, CreateError>>()?;
@@ -286,17 +264,12 @@ async fn version_create_inner(
author_id: user.id.into(),
name: version_create_data.version_title.clone(),
version_number: version_create_data.version_number.clone(),
changelog: version_create_data
.version_body
.clone()
.unwrap_or_default(),
changelog: version_create_data.version_body.clone().unwrap_or_default(),
files: Vec::new(),
dependencies,
game_versions,
loaders,
version_type: version_create_data
.release_channel
.to_string(),
version_type: version_create_data.release_channel.to_string(),
featured: version_create_data.featured,
status: version_create_data.status,
requested_status: None,
@@ -306,9 +279,7 @@ async fn version_create_inner(
}
let version = version_builder.as_mut().ok_or_else(|| {
CreateError::InvalidInput(String::from(
"`data` field must come before file fields",
))
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
})?;
let project_type = sqlx::query!(
@@ -323,12 +294,9 @@ async fn version_create_inner(
.await?
.name;
let version_data =
initial_version_data.clone().ok_or_else(|| {
CreateError::InvalidInput(
"`data` field is required".to_string(),
)
})?;
let version_data = initial_version_data
.clone()
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
upload_file(
&mut field,
@@ -365,12 +333,10 @@ async fn version_create_inner(
return Err(error);
}
let version_data = initial_version_data.ok_or_else(|| {
CreateError::InvalidInput("`data` field is required".to_string())
})?;
let builder = version_builder.ok_or_else(|| {
CreateError::InvalidInput("`data` field is required".to_string())
})?;
let version_data = initial_version_data
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
let builder = version_builder
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
if builder.files.is_empty() {
return Err(CreateError::InvalidInput(
@@ -388,9 +354,7 @@ async fn version_create_inner(
builder.project_id as crate::database::models::ids::ProjectId
)
.fetch_many(&mut *transaction)
.try_filter_map(|e| async {
Ok(e.right().map(|m| models::ids::UserId(m.follower_id)))
})
.try_filter_map(|e| async { Ok(e.right().map(|m| models::ids::UserId(m.follower_id))) })
.try_collect::<Vec<models::ids::UserId>>()
.await?;
@@ -453,8 +417,7 @@ async fn version_create_inner(
let project_id = builder.project_id;
builder.insert(transaction).await?;
models::Project::update_game_versions(project_id, &mut *transaction)
.await?;
models::Project::update_game_versions(project_id, &mut *transaction).await?;
models::Project::update_loaders(project_id, &mut *transaction).await?;
Ok(HttpResponse::Ok().json(response))
@@ -486,11 +449,8 @@ pub async fn upload_file_to_version(
.await;
if result.is_err() {
let undo_result = super::project_creation::undo_uploads(
&***file_host,
&uploaded_files,
)
.await;
let undo_result =
super::project_creation::undo_uploads(&***file_host, &uploaded_files).await;
let rollback_result = transaction.rollback().await;
undo_result?;
@@ -541,8 +501,7 @@ async fn upload_file_to_version_inner(
.await?
.ok_or_else(|| {
CreateError::CustomAuthenticationError(
"You don't have permission to upload files to this version!"
.to_string(),
"You don't have permission to upload files to this version!".to_string(),
)
})?;
@@ -551,8 +510,7 @@ async fn upload_file_to_version_inner(
.contains(Permissions::UPLOAD_VERSION)
{
return Err(CreateError::CustomAuthenticationError(
"You don't have permission to upload files to this version!"
.to_string(),
"You don't have permission to upload files to this version!".to_string(),
));
}
}
@@ -571,8 +529,7 @@ async fn upload_file_to_version_inner(
.await?
.name;
let all_game_versions =
models::categories::GameVersion::list(&mut *transaction).await?;
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
let mut error = None;
while let Some(item) = payload.next().await {
@@ -585,9 +542,7 @@ async fn upload_file_to_version_inner(
let result = async {
let content_disposition = field.content_disposition().clone();
let name = content_disposition.get_name().ok_or_else(|| {
CreateError::MissingValueError(
"Missing content name".to_string(),
)
CreateError::MissingValueError("Missing content name".to_string())
})?;
if name == "data" {
@@ -602,9 +557,7 @@ async fn upload_file_to_version_inner(
}
let file_data = initial_file_data.as_ref().ok_or_else(|| {
CreateError::InvalidInput(String::from(
"`data` field must come before file fields",
))
CreateError::InvalidInput(String::from("`data` field must come before file fields"))
})?;
let mut dependencies = version
@@ -703,9 +656,7 @@ pub async fn upload_file(
}
let content_type = crate::util::ext::project_file_type(file_extension)
.ok_or_else(|| {
CreateError::InvalidFileType(file_extension.to_string())
})?;
.ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?;
let data = read_from_field(
field, 500 * (1 << 20),
@@ -731,8 +682,7 @@ pub async fn upload_file(
if exists {
return Err(CreateError::InvalidInput(
"Duplicate files are not allowed to be uploaded to Modrinth!"
.to_string(),
"Duplicate files are not allowed to be uploaded to Modrinth!".to_string(),
));
}
@@ -761,23 +711,20 @@ pub async fn upload_file(
.collect();
let res = sqlx::query!(
"
"
SELECT v.id version_id, v.mod_id project_id, h.hash hash FROM hashes h
INNER JOIN files f on h.file_id = f.id
INNER JOIN versions v on f.version_id = v.id
WHERE h.algorithm = 'sha1' AND h.hash = ANY($1)
",
&*hashes
)
.fetch_all(&mut *transaction).await?;
&*hashes
)
.fetch_all(&mut *transaction)
.await?;
for file in &format.files {
if let Some(dep) = res.iter().find(|x| {
Some(&*x.hash)
== file
.hashes
.get(&PackFileHash::Sha1)
.map(|x| x.as_bytes())
Some(&*x.hash) == file.hashes.get(&PackFileHash::Sha1).map(|x| x.as_bytes())
}) {
dependencies.push(DependencyBuilder {
project_id: Some(models::ProjectId(dep.project_id)),
@@ -828,8 +775,7 @@ pub async fn upload_file(
version_id,
urlencoding::encode(file_name)
);
let file_path =
format!("data/{}/versions/{}/{}", project_id, version_id, &file_name);
let file_path = format!("data/{}/versions/{}/{}", project_id, version_id, &file_name);
let upload_data = file_host
.upload_file(content_type, &file_path, data)
@@ -849,8 +795,7 @@ pub async fn upload_file(
.any(|y| y.hash == sha1_bytes || y.hash == sha512_bytes)
}) {
return Err(CreateError::InvalidInput(
"Duplicate files are not allowed to be uploaded to Modrinth!"
.to_string(),
"Duplicate files are not allowed to be uploaded to Modrinth!".to_string(),
));
}
@@ -888,9 +833,9 @@ pub async fn upload_file(
pub fn get_name_ext(
content_disposition: &actix_web::http::header::ContentDisposition,
) -> Result<(&str, &str), CreateError> {
let file_name = content_disposition.get_filename().ok_or_else(|| {
CreateError::MissingValueError("Missing content file name".to_string())
})?;
let file_name = content_disposition
.get_filename()
.ok_or_else(|| CreateError::MissingValueError("Missing content file name".to_string()))?;
let file_extension = if let Some(last_period) = file_name.rfind('.') {
file_name.get((last_period + 1)..).unwrap_or("")
} else {

View File

@@ -84,8 +84,7 @@ pub async fn get_version_from_hash(
.iter()
.map(|x| database::models::VersionId(x.version_id))
.collect::<Vec<_>>();
let versions_data =
database::models::Version::get_many_full(&version_ids, &**pool).await?;
let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
if let Some(first) = versions_data.first() {
if hash_query.multiple {
@@ -96,8 +95,7 @@ pub async fn get_version_from_hash(
.collect::<Vec<_>>(),
))
} else {
Ok(HttpResponse::Ok()
.json(models::projects::Version::from(first.clone())))
Ok(HttpResponse::Ok().json(models::projects::Version::from(first.clone())))
}
} else {
Ok(HttpResponse::NotFound().body(""))
@@ -128,10 +126,16 @@ pub async fn download_version(
WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ANY($4)
ORDER BY v.date_published ASC
",
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hash.as_bytes(),
hash_query.algorithm,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_optional(&mut *transaction)
.await?;
@@ -178,28 +182,25 @@ pub async fn delete_file(
|| Some(x.version_id) == hash_query.version_id.map(|x| x.0 as i64)
}) {
if !user.role.is_admin() {
let team_member =
database::models::TeamMember::get_from_user_id_version(
database::models::ids::VersionId(row.version_id),
user.id.into(),
&**pool,
let team_member = database::models::TeamMember::get_from_user_id_version(
database::models::ids::VersionId(row.version_id),
user.id.into(),
&**pool,
)
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to delete this file!".to_string(),
)
.await
.map_err(ApiError::Database)?
.ok_or_else(|| {
ApiError::CustomAuthentication(
"You don't have permission to delete this file!"
.to_string(),
)
})?;
})?;
if !team_member
.permissions
.contains(Permissions::DELETE_VERSION)
{
return Err(ApiError::CustomAuthentication(
"You don't have permission to delete this file!"
.to_string(),
"You don't have permission to delete this file!".to_string(),
));
}
}
@@ -220,8 +221,7 @@ pub async fn delete_file(
if files.len() < 2 {
return Err(ApiError::InvalidInput(
"Versions must have at least one file uploaded to them"
.to_string(),
"Versions must have at least one file uploaded to them".to_string(),
));
}
@@ -324,9 +324,7 @@ pub async fn get_update_from_hash(
.await?;
if let Some(version_id) = version_ids.first() {
let version_data =
database::models::Version::get_full(*version_id, &**pool)
.await?;
let version_data = database::models::Version::get_full(*version_id, &**pool).await?;
ok_or_not_found::<QueryVersion, Version>(version_data)
} else {
@@ -364,10 +362,16 @@ pub async fn get_versions_from_hashes(
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)
",
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hashes_parsed.as_slice(),
file_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_all(&**pool)
.await?;
@@ -376,8 +380,7 @@ pub async fn get_versions_from_hashes(
.iter()
.map(|x| database::models::VersionId(x.version_id))
.collect::<Vec<_>>();
let versions_data =
database::models::Version::get_many_full(&version_ids, &**pool).await?;
let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
let response: Result<HashMap<String, Version>, ApiError> = result
.into_iter()
@@ -388,10 +391,7 @@ pub async fn get_versions_from_hashes(
.find(|x| x.inner.id.0 == row.version_id)
.map(|v| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok((
parsed_hash,
crate::models::projects::Version::from(v),
))
Ok((parsed_hash, crate::models::projects::Version::from(v)))
} else {
Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}",
@@ -423,20 +423,25 @@ pub async fn get_projects_from_hashes(
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)
",
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hashes_parsed.as_slice(),
file_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_all(&**pool)
.await?;
.fetch_all(&**pool)
.await?;
let project_ids = result
.iter()
.map(|x| database::models::ProjectId(x.project_id))
.collect::<Vec<_>>();
let versions_data =
database::models::Project::get_many_full(&project_ids, &**pool).await?;
let versions_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
let response: Result<HashMap<String, Project>, ApiError> = result
.into_iter()
@@ -447,10 +452,7 @@ pub async fn get_projects_from_hashes(
.find(|x| x.inner.id.0 == row.project_id)
.map(|v| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok((
parsed_hash,
crate::models::projects::Project::from(v),
))
Ok((parsed_hash, crate::models::projects::Project::from(v)))
} else {
Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}",
@@ -538,20 +540,26 @@ pub async fn update_files(
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ANY($4)
",
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hashes_parsed.as_slice(),
update_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_many(&mut *transaction)
.try_filter_map(|e| async {
Ok(e.right().map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id))))
})
.try_collect::<Vec<_>>()
.await?;
.fetch_many(&mut *transaction)
.try_filter_map(|e| async {
Ok(e.right()
.map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id))))
})
.try_collect::<Vec<_>>()
.await?;
let mut version_ids: HashMap<database::models::VersionId, Vec<u8>> =
HashMap::new();
let mut version_ids: HashMap<database::models::VersionId, Vec<u8>> = HashMap::new();
let updated_versions = database::models::Version::get_projects_versions(
result
@@ -583,17 +591,13 @@ pub async fn update_files(
.await?;
for (hash, id) in result {
if let Some(latest_version) =
updated_versions.get(&id).and_then(|x| x.last())
{
if let Some(latest_version) = updated_versions.get(&id).and_then(|x| x.last()) {
version_ids.insert(*latest_version, hash);
}
}
let query_version_ids = version_ids.keys().copied().collect::<Vec<_>>();
let versions =
database::models::Version::get_many_full(&query_version_ids, &**pool)
.await?;
let versions = database::models::Version::get_many_full(&query_version_ids, &**pool).await?;
let mut response = HashMap::new();
@@ -602,10 +606,7 @@ pub async fn update_files(
if let Some(hash) = hash {
if let Ok(parsed_hash) = String::from_utf8(hash.clone()) {
response.insert(
parsed_hash,
models::projects::Version::from(version),
);
response.insert(parsed_hash, models::projects::Version::from(version));
} else {
let version_id: VersionId = version.inner.id.into();

View File

@@ -1,13 +1,10 @@
use super::ApiError;
use crate::database;
use crate::models;
use crate::models::projects::{
Dependency, FileType, VersionStatus, VersionType,
};
use crate::models::projects::{Dependency, FileType, VersionStatus, VersionType};
use crate::models::teams::Permissions;
use crate::util::auth::{
filter_authorized_versions, get_user_from_headers, is_authorized,
is_authorized_version,
filter_authorized_versions, get_user_from_headers, is_authorized, is_authorized_version,
};
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
@@ -49,10 +46,7 @@ pub async fn version_list(
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(
&string, &**pool,
)
.await?;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -80,9 +74,7 @@ pub async fn version_list(
)
.await?;
let mut versions =
database::models::Version::get_many_full(&version_ids, &**pool)
.await?;
let mut versions = database::models::Version::get_many_full(&version_ids, &**pool).await?;
let mut response = versions
.iter()
@@ -95,22 +87,13 @@ pub async fn version_list(
.cloned()
.collect::<Vec<_>>();
versions.sort_by(|a, b| {
b.inner.date_published.cmp(&a.inner.date_published)
});
versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));
// Attempt to populate versions with "auto featured" versions
if response.is_empty()
&& !versions.is_empty()
&& filters.featured.unwrap_or(false)
{
if response.is_empty() && !versions.is_empty() && filters.featured.unwrap_or(false) {
let (loaders, game_versions) = futures::future::try_join(
database::models::categories::Loader::list(&**pool),
database::models::categories::GameVersion::list_filter(
None,
Some(true),
&**pool,
),
database::models::categories::GameVersion::list_filter(None, Some(true), &**pool),
)
.await?;
@@ -139,13 +122,10 @@ pub async fn version_list(
}
}
response.sort_by(|a, b| {
b.inner.date_published.cmp(&a.inner.date_published)
});
response.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));
response.dedup_by(|a, b| a.inner.id == b.inner.id);
let response =
filter_authorized_versions(response, &user_option, &pool).await?;
let response = filter_authorized_versions(response, &user_option, &pool).await?;
Ok(HttpResponse::Ok().json(response))
} else {
@@ -162,16 +142,13 @@ pub async fn version_project_get(
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner();
let version_data =
database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool)
.await?;
database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(data) = version_data {
if is_authorized_version(&data.inner, &user_option, &pool).await? {
return Ok(
HttpResponse::Ok().json(models::projects::Version::from(data))
);
return Ok(HttpResponse::Ok().json(models::projects::Version::from(data)));
}
}
@@ -189,18 +166,15 @@ pub async fn versions_get(
web::Query(ids): web::Query<VersionIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let version_ids =
serde_json::from_str::<Vec<models::ids::VersionId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect::<Vec<database::models::VersionId>>();
let versions_data =
database::models::Version::get_many_full(&version_ids, &**pool).await?;
let version_ids = serde_json::from_str::<Vec<models::ids::VersionId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect::<Vec<database::models::VersionId>>();
let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let versions =
filter_authorized_versions(versions_data, &user_option, &pool).await?;
let versions = filter_authorized_versions(versions_data, &user_option, &pool).await?;
Ok(HttpResponse::Ok().json(versions))
}
@@ -212,16 +186,13 @@ pub async fn version_get(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let version_data =
database::models::Version::get_full(id.into(), &**pool).await?;
let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(data) = version_data {
if is_authorized_version(&data.inner, &user_option, &pool).await? {
return Ok(
HttpResponse::Ok().json(models::projects::Version::from(data))
);
return Ok(HttpResponse::Ok().json(models::projects::Version::from(data)));
}
}
@@ -273,9 +244,9 @@ pub async fn version_edit(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_version.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;
new_version
.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let version_id = info.into_inner().0;
let id = version_id.into();
@@ -283,19 +254,15 @@ pub async fn version_edit(
let result = database::models::Version::get_full(id, &**pool).await?;
if let Some(version_item) = result {
let project_item = database::models::Project::get_full(
version_item.inner.project_id,
let project_item =
database::models::Project::get_full(version_item.inner.project_id, &**pool).await?;
let team_member = database::models::TeamMember::get_from_user_id_version(
version_item.inner.id,
user.id.into(),
&**pool,
)
.await?;
let team_member =
database::models::TeamMember::get_from_user_id_version(
version_item.inner.id,
user.id.into(),
&**pool,
)
.await?;
let permissions;
if user.role.is_admin() {
@@ -303,8 +270,7 @@ pub async fn version_edit(
} else if let Some(member) = team_member {
permissions = Some(member.permissions)
} else if user.role.is_mod() {
permissions =
Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
permissions = Some(Permissions::EDIT_DETAILS | Permissions::EDIT_BODY)
} else {
permissions = None
}
@@ -312,8 +278,7 @@ pub async fn version_edit(
if let Some(perms) = permissions {
if !perms.contains(Permissions::UPLOAD_VERSION) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit this version!"
.to_string(),
"You do not have the permissions to edit this version!".to_string(),
));
}
@@ -403,18 +368,16 @@ pub async fn version_edit(
.await?;
for game_version in game_versions {
let game_version_id =
database::models::categories::GameVersion::get_id(
&game_version.0,
&mut *transaction,
let game_version_id = database::models::categories::GameVersion::get_id(
&game_version.0,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"No database entry for game version provided.".to_string(),
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"No database entry for game version provided."
.to_string(),
)
})?;
})?;
sqlx::query!(
"
@@ -447,17 +410,13 @@ pub async fn version_edit(
for loader in loaders {
let loader_id =
database::models::categories::Loader::get_id(
&loader.0,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"No database entry for loader provided."
.to_string(),
)
})?;
database::models::categories::Loader::get_id(&loader.0, &mut *transaction)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(
"No database entry for loader provided.".to_string(),
)
})?;
sqlx::query!(
"
@@ -551,8 +510,7 @@ pub async fn version_edit(
if let Some(downloads) = &new_version.downloads {
if !user.role.is_mod() {
return Err(ApiError::CustomAuthentication(
"You don't have permission to set the downloads of this mod"
.to_string(),
"You don't have permission to set the downloads of this mod".to_string(),
));
}
@@ -577,8 +535,7 @@ pub async fn version_edit(
WHERE (id = $2)
",
diff as i32,
version_item.inner.project_id
as database::models::ids::ProjectId,
version_item.inner.project_id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
@@ -667,8 +624,7 @@ pub async fn version_schedule(
if scheduling_data.time < Utc::now() {
return Err(ApiError::InvalidInput(
"You cannot schedule a version to be released in the past!"
.to_string(),
"You cannot schedule a version to be released in the past!".to_string(),
));
}
@@ -679,17 +635,15 @@ pub async fn version_schedule(
}
let string = info.into_inner().0;
let result =
database::models::Version::get_full(string.into(), &**pool).await?;
let result = database::models::Version::get_full(string.into(), &**pool).await?;
if let Some(version_item) = result {
let team_member =
database::models::TeamMember::get_from_user_id_version(
version_item.inner.id,
user.id.into(),
&**pool,
)
.await?;
let team_member = database::models::TeamMember::get_from_user_id_version(
version_item.inner.id,
user.id.into(),
&**pool,
)
.await?;
if !user.role.is_mod()
&& !team_member
@@ -748,17 +702,14 @@ pub async fn version_delete(
.contains(Permissions::DELETE_VERSION)
{
return Err(ApiError::CustomAuthentication(
"You do not have permission to delete versions in this team"
.to_string(),
"You do not have permission to delete versions in this team".to_string(),
));
}
}
let mut transaction = pool.begin().await?;
let result =
database::models::Version::remove_full(id.into(), &mut transaction)
.await?;
let result = database::models::Version::remove_full(id.into(), &mut transaction).await?;
transaction.commit().await?;