Run fmt, fix dep route (#312)

This commit is contained in:
Geometrically
2022-02-27 21:44:00 -07:00
committed by GitHub
parent 725f8571bb
commit 459e36c027
53 changed files with 1798 additions and 867 deletions

View File

@@ -1655,38 +1655,6 @@
"nullable": [] "nullable": []
} }
}, },
"5a03c653f1ff3339a01422ee4267a66157e6da9a51cc7d9beb0f87d59c3a444c": {
"query": "\n SELECT d.dependent_id, d.dependency_id, d.mod_dependency_id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE v.mod_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "dependent_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "dependency_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "mod_dependency_id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
true,
true
]
}
},
"5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b": { "5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b": {
"query": "\n DELETE FROM reports\n WHERE version_id = $1\n ", "query": "\n DELETE FROM reports\n WHERE version_id = $1\n ",
"describe": { "describe": {
@@ -2424,6 +2392,32 @@
] ]
} }
}, },
"7eab623af88469235cad7cdf0b37bdf51eade3f5e1de25c63a8e08e55722003f": {
"query": "\n SELECT d.dependency_id, vd.mod_id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n INNER JOIN versions vd ON d.dependency_id = vd.id\n WHERE v.mod_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "dependency_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "mod_id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
false
]
}
},
"8129255d25bf0624d83f50558b668ed7b7f9c264e380d276522fc82bc871939b": { "8129255d25bf0624d83f50558b668ed7b7f9c264e380d276522fc82bc871939b": {
"query": "\n INSERT INTO notifications_actions (\n notification_id, title, action_route, action_route_method\n )\n VALUES (\n $1, $2, $3, $4\n )\n ", "query": "\n INSERT INTO notifications_actions (\n notification_id, title, action_route, action_route_method\n )\n VALUES (\n $1, $2, $3, $4\n )\n ",
"describe": { "describe": {

View File

@@ -62,7 +62,10 @@ impl Category {
} }
} }
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<CategoryId>, DatabaseError> pub async fn get_id<'a, E>(
name: &str,
exec: E,
) -> Result<Option<CategoryId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -115,7 +118,10 @@ impl Category {
Ok(result.map(|r| CategoryId(r.id))) Ok(result.map(|r| CategoryId(r.id)))
} }
pub async fn get_name<'a, E>(id: CategoryId, exec: E) -> Result<String, DatabaseError> pub async fn get_name<'a, E>(
id: CategoryId,
exec: E,
) -> Result<String, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -159,7 +165,10 @@ impl Category {
Ok(result) Ok(result)
} }
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError> pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -184,7 +193,10 @@ impl Category {
impl<'a> CategoryBuilder<'a> { impl<'a> CategoryBuilder<'a> {
/// The name of the category. Must be ASCII alphanumeric or `-`/`_` /// The name of the category. Must be ASCII alphanumeric or `-`/`_`
pub fn name(self, name: &'a str) -> Result<CategoryBuilder<'a>, DatabaseError> { pub fn name(
self,
name: &'a str,
) -> Result<CategoryBuilder<'a>, DatabaseError> {
if name if name
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
@@ -208,20 +220,26 @@ impl<'a> CategoryBuilder<'a> {
}) })
} }
pub fn icon(self, icon: &'a str) -> Result<CategoryBuilder<'a>, DatabaseError> { pub fn icon(
self,
icon: &'a str,
) -> Result<CategoryBuilder<'a>, DatabaseError> {
Ok(Self { Ok(Self {
icon: Some(icon), icon: Some(icon),
..self ..self
}) })
} }
pub async fn insert<'b, E>(self, exec: E) -> Result<CategoryId, DatabaseError> pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<CategoryId, DatabaseError>
where where
E: sqlx::Executor<'b, Database = sqlx::Postgres>, E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{ {
let id = *self let id = *self.project_type.ok_or_else(|| {
.project_type DatabaseError::Other("No project type specified.".to_string())
.ok_or_else(|| DatabaseError::Other("No project type specified.".to_string()))?; })?;
let result = sqlx::query!( let result = sqlx::query!(
" "
INSERT INTO categories (category, project_type, icon) INSERT INTO categories (category, project_type, icon)
@@ -254,7 +272,10 @@ impl Loader {
} }
} }
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<LoaderId>, DatabaseError> pub async fn get_id<'a, E>(
name: &str,
exec: E,
) -> Result<Option<LoaderId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -278,7 +299,10 @@ impl Loader {
Ok(result.map(|r| LoaderId(r.id))) Ok(result.map(|r| LoaderId(r.id)))
} }
pub async fn get_name<'a, E>(id: LoaderId, exec: E) -> Result<String, DatabaseError> pub async fn get_name<'a, E>(
id: LoaderId,
exec: E,
) -> Result<String, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -330,7 +354,10 @@ impl Loader {
} }
// TODO: remove loaders with projects using them // TODO: remove loaders with projects using them
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError> pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -355,7 +382,10 @@ impl Loader {
impl<'a> LoaderBuilder<'a> { impl<'a> LoaderBuilder<'a> {
/// The name of the loader. Must be ASCII alphanumeric or `-`/`_` /// The name of the loader. Must be ASCII alphanumeric or `-`/`_`
pub fn name(self, name: &'a str) -> Result<LoaderBuilder<'a>, DatabaseError> { pub fn name(
self,
name: &'a str,
) -> Result<LoaderBuilder<'a>, DatabaseError> {
if name if name
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
@@ -369,7 +399,10 @@ impl<'a> LoaderBuilder<'a> {
} }
} }
pub fn icon(self, icon: &'a str) -> Result<LoaderBuilder<'a>, DatabaseError> { pub fn icon(
self,
icon: &'a str,
) -> Result<LoaderBuilder<'a>, DatabaseError> {
Ok(Self { Ok(Self {
icon: Some(icon), icon: Some(icon),
..self ..self
@@ -471,7 +504,10 @@ impl GameVersion {
Ok(result.map(|r| GameVersionId(r.id))) Ok(result.map(|r| GameVersionId(r.id)))
} }
pub async fn get_name<'a, E>(id: GameVersionId, exec: E) -> Result<String, DatabaseError> pub async fn get_name<'a, E>(
id: GameVersionId,
exec: E,
) -> Result<String, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -589,7 +625,10 @@ impl GameVersion {
Ok(result) Ok(result)
} }
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError> pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -614,7 +653,10 @@ impl GameVersion {
impl<'a> GameVersionBuilder<'a> { impl<'a> GameVersionBuilder<'a> {
/// The game version. Spaces must be replaced with '_' for it to be valid /// The game version. Spaces must be replaced with '_' for it to be valid
pub fn version(self, version: &'a str) -> Result<GameVersionBuilder<'a>, DatabaseError> { pub fn version(
self,
version: &'a str,
) -> Result<GameVersionBuilder<'a>, DatabaseError> {
if version if version
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
@@ -645,14 +687,20 @@ impl<'a> GameVersionBuilder<'a> {
} }
} }
pub fn created(self, created: &'a chrono::DateTime<chrono::Utc>) -> GameVersionBuilder<'a> { pub fn created(
self,
created: &'a chrono::DateTime<chrono::Utc>,
) -> GameVersionBuilder<'a> {
Self { Self {
date: Some(created), date: Some(created),
..self ..self
} }
} }
pub async fn insert<'b, E>(self, exec: E) -> Result<GameVersionId, DatabaseError> pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<GameVersionId, DatabaseError>
where where
E: sqlx::Executor<'b, Database = sqlx::Postgres>, E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{ {
@@ -690,7 +738,10 @@ impl License {
LicenseBuilder::default() LicenseBuilder::default()
} }
pub async fn get_id<'a, E>(id: &str, exec: E) -> Result<Option<LicenseId>, DatabaseError> pub async fn get_id<'a, E>(
id: &str,
exec: E,
) -> Result<Option<LicenseId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -707,7 +758,10 @@ impl License {
Ok(result.map(|r| LicenseId(r.id))) Ok(result.map(|r| LicenseId(r.id)))
} }
pub async fn get<'a, E>(id: LicenseId, exec: E) -> Result<License, DatabaseError> pub async fn get<'a, E>(
id: LicenseId,
exec: E,
) -> Result<License, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -751,7 +805,10 @@ impl License {
Ok(result) Ok(result)
} }
pub async fn remove<'a, E>(short: &str, exec: E) -> Result<Option<()>, DatabaseError> pub async fn remove<'a, E>(
short: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -776,7 +833,10 @@ impl License {
impl<'a> LicenseBuilder<'a> { impl<'a> LicenseBuilder<'a> {
/// The license's short name/abbreviation. Spaces must be replaced with '_' for it to be valid /// The license's short name/abbreviation. Spaces must be replaced with '_' for it to be valid
pub fn short(self, short: &'a str) -> Result<LicenseBuilder<'a>, DatabaseError> { pub fn short(
self,
short: &'a str,
) -> Result<LicenseBuilder<'a>, DatabaseError> {
if short if short
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
@@ -791,14 +851,20 @@ impl<'a> LicenseBuilder<'a> {
} }
/// The license's long name /// The license's long name
pub fn name(self, name: &'a str) -> Result<LicenseBuilder<'a>, DatabaseError> { pub fn name(
self,
name: &'a str,
) -> Result<LicenseBuilder<'a>, DatabaseError> {
Ok(Self { Ok(Self {
name: Some(name), name: Some(name),
..self ..self
}) })
} }
pub async fn insert<'b, E>(self, exec: E) -> Result<LicenseId, DatabaseError> pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<LicenseId, DatabaseError>
where where
E: sqlx::Executor<'b, Database = sqlx::Postgres>, E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{ {
@@ -874,7 +940,9 @@ impl DonationPlatform {
}) })
} }
pub async fn list<'a, E>(exec: E) -> Result<Vec<DonationPlatform>, DatabaseError> pub async fn list<'a, E>(
exec: E,
) -> Result<Vec<DonationPlatform>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -897,7 +965,10 @@ impl DonationPlatform {
Ok(result) Ok(result)
} }
pub async fn remove<'a, E>(short: &str, exec: E) -> Result<Option<()>, DatabaseError> pub async fn remove<'a, E>(
short: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -922,7 +993,10 @@ impl DonationPlatform {
impl<'a> DonationPlatformBuilder<'a> { impl<'a> DonationPlatformBuilder<'a> {
/// The donation platform short name. Spaces must be replaced with '_' for it to be valid /// The donation platform short name. Spaces must be replaced with '_' for it to be valid
pub fn short(self, short: &'a str) -> Result<DonationPlatformBuilder<'a>, DatabaseError> { pub fn short(
self,
short: &'a str,
) -> Result<DonationPlatformBuilder<'a>, DatabaseError> {
if short if short
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
@@ -937,14 +1011,20 @@ impl<'a> DonationPlatformBuilder<'a> {
} }
/// The donation platform long name /// The donation platform long name
pub fn name(self, name: &'a str) -> Result<DonationPlatformBuilder<'a>, DatabaseError> { pub fn name(
self,
name: &'a str,
) -> Result<DonationPlatformBuilder<'a>, DatabaseError> {
Ok(Self { Ok(Self {
name: Some(name), name: Some(name),
..self ..self
}) })
} }
pub async fn insert<'b, E>(self, exec: E) -> Result<DonationPlatformId, DatabaseError> pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<DonationPlatformId, DatabaseError>
where where
E: sqlx::Executor<'b, Database = sqlx::Postgres>, E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{ {
@@ -974,7 +1054,10 @@ impl ReportType {
ReportTypeBuilder { name: None } ReportTypeBuilder { name: None }
} }
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ReportTypeId>, DatabaseError> pub async fn get_id<'a, E>(
name: &str,
exec: E,
) -> Result<Option<ReportTypeId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -998,7 +1081,10 @@ impl ReportType {
Ok(result.map(|r| ReportTypeId(r.id))) Ok(result.map(|r| ReportTypeId(r.id)))
} }
pub async fn get_name<'a, E>(id: ReportTypeId, exec: E) -> Result<String, DatabaseError> pub async fn get_name<'a, E>(
id: ReportTypeId,
exec: E,
) -> Result<String, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -1032,7 +1118,10 @@ impl ReportType {
Ok(result) Ok(result)
} }
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError> pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -1057,7 +1146,10 @@ impl ReportType {
impl<'a> ReportTypeBuilder<'a> { impl<'a> ReportTypeBuilder<'a> {
/// The name of the report type. Must be ASCII alphanumeric or `-`/`_` /// The name of the report type. Must be ASCII alphanumeric or `-`/`_`
pub fn name(self, name: &'a str) -> Result<ReportTypeBuilder<'a>, DatabaseError> { pub fn name(
self,
name: &'a str,
) -> Result<ReportTypeBuilder<'a>, DatabaseError> {
if name if name
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
@@ -1068,7 +1160,10 @@ impl<'a> ReportTypeBuilder<'a> {
} }
} }
pub async fn insert<'b, E>(self, exec: E) -> Result<ReportTypeId, DatabaseError> pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<ReportTypeId, DatabaseError>
where where
E: sqlx::Executor<'b, Database = sqlx::Postgres>, E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{ {
@@ -1097,7 +1192,10 @@ impl ProjectType {
ProjectTypeBuilder { name: None } ProjectTypeBuilder { name: None }
} }
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ProjectTypeId>, DatabaseError> pub async fn get_id<'a, E>(
name: &str,
exec: E,
) -> Result<Option<ProjectTypeId>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -1148,7 +1246,10 @@ impl ProjectType {
Ok(project_types) Ok(project_types)
} }
pub async fn get_name<'a, E>(id: ProjectTypeId, exec: E) -> Result<String, DatabaseError> pub async fn get_name<'a, E>(
id: ProjectTypeId,
exec: E,
) -> Result<String, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -1183,7 +1284,10 @@ impl ProjectType {
} }
// TODO: remove loaders with mods using them // TODO: remove loaders with mods using them
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError> pub async fn remove<'a, E>(
name: &str,
exec: E,
) -> Result<Option<()>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -1208,7 +1312,10 @@ impl ProjectType {
impl<'a> ProjectTypeBuilder<'a> { impl<'a> ProjectTypeBuilder<'a> {
/// The name of the project type. Must be ASCII alphanumeric or `-`/`_` /// The name of the project type. Must be ASCII alphanumeric or `-`/`_`
pub fn name(self, name: &'a str) -> Result<ProjectTypeBuilder<'a>, DatabaseError> { pub fn name(
self,
name: &'a str,
) -> Result<ProjectTypeBuilder<'a>, DatabaseError> {
if name if name
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
@@ -1219,7 +1326,10 @@ impl<'a> ProjectTypeBuilder<'a> {
} }
} }
pub async fn insert<'b, E>(self, exec: E) -> Result<ProjectTypeId, DatabaseError> pub async fn insert<'b, E>(
self,
exec: E,
) -> Result<ProjectTypeId, DatabaseError>
where where
E: sqlx::Executor<'b, Database = sqlx::Postgres>, E: sqlx::Executor<'b, Database = sqlx::Postgres>,
{ {

View File

@@ -83,7 +83,10 @@ impl ids::SideTypeId {
} }
impl ids::DonationPlatformId { impl ids::DonationPlatformId {
pub async fn get_id<'a, E>(id: &str, exec: E) -> Result<Option<Self>, DatabaseError> pub async fn get_id<'a, E>(
id: &str,
exec: E,
) -> Result<Option<Self>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -102,7 +105,10 @@ impl ids::DonationPlatformId {
} }
impl ids::ProjectTypeId { impl ids::ProjectTypeId {
pub async fn get_id<'a, E>(project_type: String, exec: E) -> Result<Option<Self>, DatabaseError> pub async fn get_id<'a, E>(
project_type: String,
exec: E,
) -> Result<Option<Self>, DatabaseError>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {

View File

@@ -174,9 +174,11 @@ impl Notification {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
futures::future::try_join_all(notification_ids.into_iter().map(|id| Self::get(id, exec))) futures::future::try_join_all(
.await notification_ids.into_iter().map(|id| Self::get(id, exec)),
.map(|x| x.into_iter().flatten().collect()) )
.await
.map(|x| x.into_iter().flatten().collect())
} }
pub async fn get_many_user<'a, E>( pub async fn get_many_user<'a, E>(
@@ -234,7 +236,8 @@ impl Notification {
notification_ids: Vec<NotificationId>, notification_ids: Vec<NotificationId>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> { ) -> Result<Option<()>, sqlx::error::Error> {
let notification_ids_parsed: Vec<i64> = notification_ids.into_iter().map(|x| x.0).collect(); let notification_ids_parsed: Vec<i64> =
notification_ids.into_iter().map(|x| x.0).collect();
sqlx::query!( sqlx::query!(
" "

View File

@@ -300,7 +300,8 @@ impl Project {
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let project_ids_parsed: Vec<i64> = project_ids.into_iter().map(|x| x.0).collect(); let project_ids_parsed: Vec<i64> =
project_ids.into_iter().map(|x| x.0).collect();
let projects = sqlx::query!( let projects = sqlx::query!(
" "
SELECT id, project_type, title, description, downloads, follows, SELECT id, project_type, title, description, downloads, follows,
@@ -542,19 +543,24 @@ impl Project {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
let id_option = let id_option = crate::models::ids::base62_impl::parse_base62(
crate::models::ids::base62_impl::parse_base62(&*slug_or_project_id.clone()).ok(); &*slug_or_project_id.clone(),
)
.ok();
if let Some(id) = id_option { if let Some(id) = id_option {
let mut project = Project::get(ProjectId(id as i64), executor).await?; let mut project =
Project::get(ProjectId(id as i64), executor).await?;
if project.is_none() { if project.is_none() {
project = Project::get_from_slug(&slug_or_project_id, executor).await?; project = Project::get_from_slug(&slug_or_project_id, executor)
.await?;
} }
Ok(project) Ok(project)
} else { } else {
let project = Project::get_from_slug(&slug_or_project_id, executor).await?; let project =
Project::get_from_slug(&slug_or_project_id, executor).await?;
Ok(project) Ok(project)
} }
@@ -567,18 +573,25 @@ impl Project {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
let id_option = crate::models::ids::base62_impl::parse_base62(slug_or_project_id).ok(); let id_option =
crate::models::ids::base62_impl::parse_base62(slug_or_project_id)
.ok();
if let Some(id) = id_option { if let Some(id) = id_option {
let mut project = Project::get_full(ProjectId(id as i64), executor).await?; let mut project =
Project::get_full(ProjectId(id as i64), executor).await?;
if project.is_none() { if project.is_none() {
project = Project::get_full_from_slug(slug_or_project_id, executor).await?; project =
Project::get_full_from_slug(slug_or_project_id, executor)
.await?;
} }
Ok(project) Ok(project)
} else { } else {
let project = Project::get_full_from_slug(slug_or_project_id, executor).await?; let project =
Project::get_full_from_slug(slug_or_project_id, executor)
.await?;
Ok(project) Ok(project)
} }
} }
@@ -676,8 +689,14 @@ impl Project {
moderation_message_body: m.moderation_message_body, moderation_message_body: m.moderation_message_body,
}, },
project_type: m.project_type_name, project_type: m.project_type_name,
categories: categories?.into_iter().map(|x| x.category).collect(), categories: categories?
versions: versions?.into_iter().map(|x| VersionId(x.id)).collect(), .into_iter()
.map(|x| x.category)
.collect(),
versions: versions?
.into_iter()
.map(|x| VersionId(x.id))
.collect(),
donation_urls: donations? donation_urls: donations?
.into_iter() .into_iter()
.map(|x| DonationUrl { .map(|x| DonationUrl {
@@ -699,11 +718,17 @@ impl Project {
created: x.created, created: x.created,
}) })
.collect(), .collect(),
status: crate::models::projects::ProjectStatus::from_str(&m.status_name), status: crate::models::projects::ProjectStatus::from_str(
&m.status_name,
),
license_id: m.short, license_id: m.short,
license_name: m.license_name, license_name: m.license_name,
client_side: crate::models::projects::SideType::from_str(&m.client_side_type), client_side: crate::models::projects::SideType::from_str(
server_side: crate::models::projects::SideType::from_str(&m.server_side_type), &m.client_side_type,
),
server_side: crate::models::projects::SideType::from_str(
&m.server_side_type,
),
})) }))
} else { } else {
Ok(None) Ok(None)
@@ -717,9 +742,11 @@ impl Project {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
futures::future::try_join_all(project_ids.into_iter().map(|id| Self::get_full(id, exec))) futures::future::try_join_all(
.await project_ids.into_iter().map(|id| Self::get_full(id, exec)),
.map(|x| x.into_iter().flatten().collect()) )
.await
.map(|x| x.into_iter().flatten().collect())
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View File

@@ -52,7 +52,10 @@ impl Report {
Ok(()) Ok(())
} }
pub async fn get<'a, E>(id: ReportId, exec: E) -> Result<Option<QueryReport>, sqlx::Error> pub async fn get<'a, E>(
id: ReportId,
exec: E,
) -> Result<Option<QueryReport>, sqlx::Error>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
@@ -93,7 +96,8 @@ impl Report {
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let report_ids_parsed: Vec<i64> = report_ids.into_iter().map(|x| x.0).collect(); let report_ids_parsed: Vec<i64> =
report_ids.into_iter().map(|x| x.0).collect();
let reports = sqlx::query!( let reports = sqlx::query!(
" "
SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created
@@ -123,7 +127,10 @@ impl Report {
Ok(reports) Ok(reports)
} }
pub async fn remove_full<'a, E>(id: ReportId, exec: E) -> Result<Option<()>, sqlx::Error> pub async fn remove_full<'a, E>(
id: ReportId,
exec: E,
) -> Result<Option<()>, sqlx::Error>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {

View File

@@ -32,7 +32,8 @@ impl TeamBuilder {
.await?; .await?;
for member in self.members { for member in self.members {
let team_member_id = generate_team_member_id(&mut *transaction).await?; let team_member_id =
generate_team_member_id(&mut *transaction).await?;
let team_member = TeamMember { let team_member = TeamMember {
id: team_member_id, id: team_member_id,
team_id, team_id,

View File

@@ -42,7 +42,10 @@ impl User {
Ok(()) Ok(())
} }
pub async fn get<'a, 'b, E>(id: UserId, executor: E) -> Result<Option<Self>, sqlx::error::Error> pub async fn get<'a, 'b, E>(
id: UserId,
executor: E,
) -> Result<Option<Self>, sqlx::error::Error>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres>, E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{ {
@@ -150,13 +153,17 @@ impl User {
} }
} }
pub async fn get_many<'a, E>(user_ids: Vec<UserId>, exec: E) -> Result<Vec<User>, sqlx::Error> pub async fn get_many<'a, E>(
user_ids: Vec<UserId>,
exec: E,
) -> Result<Vec<User>, sqlx::Error>
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let user_ids_parsed: Vec<i64> = user_ids.into_iter().map(|x| x.0).collect(); let user_ids_parsed: Vec<i64> =
user_ids.into_iter().map(|x| x.0).collect();
let users = sqlx::query!( let users = sqlx::query!(
" "
SELECT u.id, u.github_id, u.name, u.email, SELECT u.id, u.github_id, u.name, u.email,
@@ -365,8 +372,11 @@ impl User {
.await?; .await?;
for project_id in projects { for project_id in projects {
let _result = let _result = super::project_item::Project::remove_full(
super::project_item::Project::remove_full(project_id, transaction).await?; project_id,
transaction,
)
.await?;
} }
let notifications: Vec<i64> = sqlx::query!( let notifications: Vec<i64> = sqlx::query!(
@@ -445,7 +455,8 @@ impl User {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
let id_option = crate::models::ids::base62_impl::parse_base62(username_or_id).ok(); let id_option =
crate::models::ids::base62_impl::parse_base62(username_or_id).ok();
if let Some(id) = id_option { if let Some(id) = id_option {
let id = UserId(id as i64); let id = UserId(id as i64);

View File

@@ -29,11 +29,13 @@ impl DependencyBuilder {
version_id: VersionId, version_id: VersionId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> { ) -> Result<(), DatabaseError> {
let (version_dependency_id, project_dependency_id): (Option<VersionId>, Option<ProjectId>) = let (version_dependency_id, project_dependency_id): (
if self.version_id.is_some() { Option<VersionId>,
(self.version_id, None) Option<ProjectId>,
} else if let Some(project_id) = self.project_id { ) = if self.version_id.is_some() {
let version_id = sqlx::query!( (self.version_id, None)
} else if let Some(project_id) = self.project_id {
let version_id = sqlx::query!(
" "
SELECT version.id id FROM ( SELECT version.id id FROM (
SELECT DISTINCT ON(v.id) v.id, v.date_published FROM versions v SELECT DISTINCT ON(v.id) v.id, v.date_published FROM versions v
@@ -49,10 +51,10 @@ impl DependencyBuilder {
) )
.fetch_optional(&mut *transaction).await?.map(|x| VersionId(x.id)); .fetch_optional(&mut *transaction).await?.map(|x| VersionId(x.id));
(version_id, Some(project_id)) (version_id, Some(project_id))
} else { } else {
(None, None) (None, None)
}; };
sqlx::query!( sqlx::query!(
" "
@@ -561,7 +563,8 @@ impl Version {
{ {
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
let version_ids_parsed: Vec<i64> = version_ids.into_iter().map(|x| x.0).collect(); let version_ids_parsed: Vec<i64> =
version_ids.into_iter().map(|x| x.0).collect();
let versions = sqlx::query!( let versions = sqlx::query!(
" "
SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number, SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,
@@ -662,7 +665,8 @@ impl Version {
); );
if let Some(v) = version? { if let Some(v) = version? {
let mut hashes_map: HashMap<FileId, HashMap<String, Vec<u8>>> = HashMap::new(); let mut hashes_map: HashMap<FileId, HashMap<String, Vec<u8>>> =
HashMap::new();
for hash in hashes? { for hash in hashes? {
let entry = hashes_map let entry = hashes_map
@@ -690,11 +694,17 @@ impl Version {
id: FileId(x.id), id: FileId(x.id),
url: x.url, url: x.url,
filename: x.filename, filename: x.filename,
hashes: hashes_map.entry(FileId(x.id)).or_default().clone(), hashes: hashes_map
.entry(FileId(x.id))
.or_default()
.clone(),
primary: x.is_primary, primary: x.is_primary,
}) })
.collect(), .collect(),
game_versions: game_versions?.into_iter().map(|x| x.game_version).collect(), game_versions: game_versions?
.into_iter()
.map(|x| x.game_version)
.collect(),
loaders: loaders?.into_iter().map(|x| x.loader).collect(), loaders: loaders?.into_iter().map(|x| x.loader).collect(),
featured: v.featured, featured: v.featured,
dependencies: dependencies? dependencies: dependencies?
@@ -719,9 +729,11 @@ impl Version {
where where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{ {
futures::future::try_join_all(version_ids.into_iter().map(|id| Self::get_full(id, exec))) futures::future::try_join_all(
.await version_ids.into_iter().map(|id| Self::get_full(id, exec)),
.map(|x| x.into_iter().flatten().collect()) )
.await
.map(|x| x.into_iter().flatten().collect())
} }
} }

View File

@@ -5,7 +5,8 @@ use sqlx::{Connection, PgConnection, Postgres};
pub async fn connect() -> Result<PgPool, sqlx::Error> { pub async fn connect() -> Result<PgPool, sqlx::Error> {
info!("Initializing database connection"); info!("Initializing database connection");
let database_url = dotenv::var("DATABASE_URL").expect("`DATABASE_URL` not in .env"); let database_url =
dotenv::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.min_connections( .min_connections(
dotenv::var("DATABASE_MIN_CONNECTIONS") dotenv::var("DATABASE_MIN_CONNECTIONS")

View File

@@ -16,10 +16,12 @@ pub struct BackblazeHost {
impl BackblazeHost { impl BackblazeHost {
pub async fn new(key_id: &str, key: &str, bucket_id: &str) -> Self { pub async fn new(key_id: &str, key: &str, bucket_id: &str) -> Self {
let authorization_data = authorization::authorize_account(key_id, key).await.unwrap(); let authorization_data =
let upload_url_data = authorization::get_upload_url(&authorization_data, bucket_id) authorization::authorize_account(key_id, key).await.unwrap();
.await let upload_url_data =
.unwrap(); authorization::get_upload_url(&authorization_data, bucket_id)
.await
.unwrap();
BackblazeHost { BackblazeHost {
upload_url_data, upload_url_data,
@@ -38,8 +40,13 @@ impl FileHost for BackblazeHost {
) -> Result<UploadFileData, FileHostingError> { ) -> Result<UploadFileData, FileHostingError> {
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes));
let upload_data = let upload_data = upload::upload_file(
upload::upload_file(&self.upload_url_data, content_type, file_name, file_bytes).await?; &self.upload_url_data,
content_type,
file_name,
file_bytes,
)
.await?;
Ok(UploadFileData { Ok(UploadFileData {
file_id: upload_data.file_id, file_id: upload_data.file_id,
file_name: upload_data.file_name, file_name: upload_data.file_name,
@@ -74,8 +81,12 @@ impl FileHost for BackblazeHost {
file_id: &str, file_id: &str,
file_name: &str, file_name: &str,
) -> Result<DeleteFileData, FileHostingError> { ) -> Result<DeleteFileData, FileHostingError> {
let delete_data = let delete_data = delete::delete_file_version(
delete::delete_file_version(&self.authorization_data, file_id, file_name).await?; &self.authorization_data,
file_id,
file_name,
)
.await?;
Ok(DeleteFileData { Ok(DeleteFileData {
file_id: delete_data.file_id, file_id: delete_data.file_id,
file_name: delete_data.file_name, file_name: delete_data.file_name,
@@ -83,7 +94,9 @@ impl FileHost for BackblazeHost {
} }
} }
pub async fn process_response<T>(response: Response) -> Result<T, FileHostingError> pub async fn process_response<T>(
response: Response,
) -> Result<T, FileHostingError>
where where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
{ {

View File

@@ -52,7 +52,13 @@ pub async fn get_upload_url(
bucket_id: &str, bucket_id: &str,
) -> Result<UploadUrlData, FileHostingError> { ) -> Result<UploadUrlData, FileHostingError> {
let response = reqwest::Client::new() let response = reqwest::Client::new()
.post(&format!("{}/b2api/v2/b2_get_upload_url", authorization_data.api_url).to_string()) .post(
&format!(
"{}/b2api/v2/b2_get_upload_url",
authorization_data.api_url
)
.to_string(),
)
.header(reqwest::header::CONTENT_TYPE, "application/json") .header(reqwest::header::CONTENT_TYPE, "application/json")
.header( .header(
reqwest::header::AUTHORIZATION, reqwest::header::AUTHORIZATION,

View File

@@ -19,11 +19,15 @@ impl FileHost for MockHost {
file_name: &str, file_name: &str,
file_bytes: Bytes, file_bytes: Bytes,
) -> Result<UploadFileData, FileHostingError> { ) -> Result<UploadFileData, FileHostingError> {
let path = std::path::Path::new(&dotenv::var("MOCK_FILE_PATH").unwrap()) let path =
.join(file_name.replace("../", "")); std::path::Path::new(&dotenv::var("MOCK_FILE_PATH").unwrap())
std::fs::create_dir_all(path.parent().ok_or(FileHostingError::InvalidFilename)?)?; .join(file_name.replace("../", ""));
std::fs::create_dir_all(
path.parent().ok_or(FileHostingError::InvalidFilename)?,
)?;
let content_sha1 = sha1::Sha1::from(&*file_bytes).hexdigest(); let content_sha1 = sha1::Sha1::from(&*file_bytes).hexdigest();
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&*file_bytes)); let content_sha512 =
format!("{:x}", sha2::Sha512::digest(&*file_bytes));
std::fs::write(path, &*file_bytes)?; std::fs::write(path, &*file_bytes)?;
Ok(UploadFileData { Ok(UploadFileData {
@@ -43,8 +47,9 @@ impl FileHost for MockHost {
file_id: &str, file_id: &str,
file_name: &str, file_name: &str,
) -> Result<DeleteFileData, FileHostingError> { ) -> Result<DeleteFileData, FileHostingError> {
let path = std::path::Path::new(&dotenv::var("MOCK_FILE_PATH").unwrap()) let path =
.join(file_name.replace("../", "")); std::path::Path::new(&dotenv::var("MOCK_FILE_PATH").unwrap())
.join(file_name.replace("../", ""));
std::fs::remove_file(path)?; std::fs::remove_file(path)?;
Ok(DeleteFileData { Ok(DeleteFileData {

View File

@@ -1,4 +1,6 @@
use crate::file_hosting::{DeleteFileData, FileHost, FileHostingError, UploadFileData}; use crate::file_hosting::{
DeleteFileData, FileHost, FileHostingError, UploadFileData,
};
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use s3::bucket::Bucket; use s3::bucket::Bucket;
@@ -24,12 +26,23 @@ impl S3Host {
region: bucket_region.to_string(), region: bucket_region.to_string(),
endpoint: url.to_string(), endpoint: url.to_string(),
}, },
Credentials::new(Some(access_token), Some(secret), None, None, None).map_err(|_| { Credentials::new(
FileHostingError::S3Error("Error while creating credentials".to_string()) Some(access_token),
Some(secret),
None,
None,
None,
)
.map_err(|_| {
FileHostingError::S3Error(
"Error while creating credentials".to_string(),
)
})?, })?,
) )
.map_err(|_| { .map_err(|_| {
FileHostingError::S3Error("Error while creating Bucket instance".to_string()) FileHostingError::S3Error(
"Error while creating Bucket instance".to_string(),
)
})?; })?;
bucket.add_header("x-amz-acl", "public-read"); bucket.add_header("x-amz-acl", "public-read");
@@ -47,13 +60,20 @@ impl FileHost for S3Host {
file_bytes: Bytes, file_bytes: Bytes,
) -> Result<UploadFileData, FileHostingError> { ) -> Result<UploadFileData, FileHostingError> {
let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest(); let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest();
let content_sha512 = format!("{:x}", sha2::Sha512::digest(&*file_bytes)); let content_sha512 =
format!("{:x}", sha2::Sha512::digest(&*file_bytes));
self.bucket self.bucket
.put_object_with_content_type(format!("/{}", file_name), &*file_bytes, content_type) .put_object_with_content_type(
format!("/{}", file_name),
&*file_bytes,
content_type,
)
.await .await
.map_err(|_| { .map_err(|_| {
FileHostingError::S3Error("Error while uploading file to S3".to_string()) FileHostingError::S3Error(
"Error while uploading file to S3".to_string(),
)
})?; })?;
Ok(UploadFileData { Ok(UploadFileData {
@@ -77,7 +97,9 @@ impl FileHost for S3Host {
.delete_object(format!("/{}", file_name)) .delete_object(format!("/{}", file_name))
.await .await
.map_err(|_| { .map_err(|_| {
FileHostingError::S3Error("Error while deleting file from S3".to_string()) FileHostingError::S3Error(
"Error while deleting file from S3".to_string(),
)
})?; })?;
Ok(DeleteFileData { Ok(DeleteFileData {

View File

@@ -1,7 +1,9 @@
use actix_web::web; use actix_web::web;
use sqlx::PgPool; use sqlx::PgPool;
pub async fn test_database(postgres: web::Data<PgPool>) -> Result<(), sqlx::Error> { pub async fn test_database(
postgres: web::Data<PgPool>,
) -> Result<(), sqlx::Error> {
let mut transaction = postgres.acquire().await?; let mut transaction = postgres.acquire().await?;
sqlx::query( sqlx::query(
" "

View File

@@ -51,7 +51,8 @@ pub struct Pepper {
#[actix_rt::main] #[actix_rt::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); env_logger::Builder::from_env(Env::default().default_filter_or("info"))
.init();
let config = Config::parse_args_default_or_exit(); let config = Config::parse_args_default_or_exit();
@@ -102,37 +103,40 @@ async fn main() -> std::io::Result<()> {
.await .await
.expect("Database connection failed"); .expect("Database connection failed");
let storage_backend = dotenv::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string()); let storage_backend =
dotenv::var("STORAGE_BACKEND").unwrap_or_else(|_| "local".to_string());
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> = match storage_backend.as_str() { let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> =
"backblaze" => Arc::new( match storage_backend.as_str() {
file_hosting::BackblazeHost::new( "backblaze" => Arc::new(
&dotenv::var("BACKBLAZE_KEY_ID").unwrap(), file_hosting::BackblazeHost::new(
&dotenv::var("BACKBLAZE_KEY").unwrap(), &dotenv::var("BACKBLAZE_KEY_ID").unwrap(),
&dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(), &dotenv::var("BACKBLAZE_KEY").unwrap(),
) &dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(),
.await, )
), .await,
"s3" => Arc::new( ),
S3Host::new( "s3" => Arc::new(
&*dotenv::var("S3_BUCKET_NAME").unwrap(), S3Host::new(
&*dotenv::var("S3_REGION").unwrap(), &*dotenv::var("S3_BUCKET_NAME").unwrap(),
&*dotenv::var("S3_URL").unwrap(), &*dotenv::var("S3_REGION").unwrap(),
&*dotenv::var("S3_ACCESS_TOKEN").unwrap(), &*dotenv::var("S3_URL").unwrap(),
&*dotenv::var("S3_SECRET").unwrap(), &*dotenv::var("S3_ACCESS_TOKEN").unwrap(),
) &*dotenv::var("S3_SECRET").unwrap(),
.unwrap(), )
), .unwrap(),
"local" => Arc::new(file_hosting::MockHost::new()), ),
_ => panic!("Invalid storage backend specified. Aborting startup!"), "local" => Arc::new(file_hosting::MockHost::new()),
}; _ => panic!("Invalid storage backend specified. Aborting startup!"),
};
let mut scheduler = scheduler::Scheduler::new(); let mut scheduler = scheduler::Scheduler::new();
// The interval in seconds at which the local database is indexed // The interval in seconds at which the local database is indexed
// for searching. Defaults to 1 hour if unset. // for searching. Defaults to 1 hour if unset.
let local_index_interval = let local_index_interval = std::time::Duration::from_secs(
std::time::Duration::from_secs(parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600)); parse_var("LOCAL_INDEX_INTERVAL").unwrap_or(3600),
);
let mut skip = skip_initial; let mut skip = skip_initial;
let pool_ref = pool.clone(); let pool_ref = pool.clone();
@@ -150,7 +154,8 @@ async fn main() -> std::io::Result<()> {
} }
info!("Indexing local database"); info!("Indexing local database");
let settings = IndexingSettings { index_local: true }; let settings = IndexingSettings { index_local: true };
let result = index_projects(pool_ref, settings, &search_config_ref).await; let result =
index_projects(pool_ref, settings, &search_config_ref).await;
if let Err(e) = result { if let Err(e) = result {
warn!("Local project indexing failed: {:?}", e); warn!("Local project indexing failed: {:?}", e);
} }
@@ -185,7 +190,8 @@ async fn main() -> std::io::Result<()> {
} }
}); });
let indexing_queue = Arc::new(search::indexing::queue::CreationQueue::new()); let indexing_queue =
Arc::new(search::indexing::queue::CreationQueue::new());
let mut skip = skip_initial; let mut skip = skip_initial;
let queue_ref = indexing_queue.clone(); let queue_ref = indexing_queue.clone();
@@ -214,7 +220,10 @@ async fn main() -> std::io::Result<()> {
scheduler::schedule_versions(&mut scheduler, pool.clone(), skip_initial); scheduler::schedule_versions(&mut scheduler, pool.clone(), skip_initial);
let ip_salt = Pepper { let ip_salt = Pepper {
pepper: crate::models::ids::Base62Id(crate::models::ids::random_base62(11)).to_string(), pepper: crate::models::ids::Base62Id(
crate::models::ids::random_base62(11),
)
.to_string(),
}; };
let store = MemoryStore::new(); let store = MemoryStore::new();
@@ -236,10 +245,16 @@ async fn main() -> std::io::Result<()> {
RateLimiter::new(MemoryStoreActor::from(store.clone()).start()) RateLimiter::new(MemoryStoreActor::from(store.clone()).start())
.with_identifier(|req| { .with_identifier(|req| {
let connection_info = req.connection_info(); let connection_info = req.connection_info();
let ip = let ip = String::from(
String::from(if parse_var("CLOUDFLARE_INTEGRATION").unwrap_or(false) { if parse_var("CLOUDFLARE_INTEGRATION")
if let Some(header) = req.headers().get("CF-Connecting-IP") { .unwrap_or(false)
header.to_str().map_err(|_| ARError::IdentificationError)? {
if let Some(header) =
req.headers().get("CF-Connecting-IP")
{
header.to_str().map_err(|_| {
ARError::IdentificationError
})?
} else { } else {
connection_info connection_info
.peer_addr() .peer_addr()
@@ -249,14 +264,16 @@ async fn main() -> std::io::Result<()> {
connection_info connection_info
.peer_addr() .peer_addr()
.ok_or(ARError::IdentificationError)? .ok_or(ARError::IdentificationError)?
}); },
);
Ok(ip) Ok(ip)
}) })
.with_interval(std::time::Duration::from_secs(60)) .with_interval(std::time::Duration::from_secs(60))
.with_max_requests(300) .with_max_requests(300)
.with_ignore_ips( .with_ignore_ips(
parse_strings_from_var("RATE_LIMIT_IGNORE_IPS").unwrap_or_default(), parse_strings_from_var("RATE_LIMIT_IGNORE_IPS")
.unwrap_or_default(),
), ),
) )
.app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(pool.clone()))

View File

@@ -127,7 +127,10 @@ pub mod base62_impl {
impl<'de> Visitor<'de> for Base62Visitor { impl<'de> Visitor<'de> for Base62Visitor {
type Value = Base62Id; type Value = Base62Id;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
formatter.write_str("a base62 string id") formatter.write_str("a base62 string id")
} }
@@ -183,7 +186,9 @@ pub mod base62_impl {
} }
// We don't want this panicking or wrapping on integer overflow // We don't want this panicking or wrapping on integer overflow
if let Some(n) = num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) { if let Some(n) =
num.checked_mul(62).and_then(|n| n.checked_add(next_digit))
{
num = n; num = n;
} else { } else {
return Err(DecodingError::Overflow); return Err(DecodingError::Overflow);

View File

@@ -360,10 +360,16 @@ impl From<QueryVersion> for Version {
.map(|d| Dependency { .map(|d| Dependency {
version_id: d.version_id.map(|i| VersionId(i.0 as u64)), version_id: d.version_id.map(|i| VersionId(i.0 as u64)),
project_id: d.project_id.map(|i| ProjectId(i.0 as u64)), project_id: d.project_id.map(|i| ProjectId(i.0 as u64)),
dependency_type: DependencyType::from_str(d.dependency_type.as_str()), dependency_type: DependencyType::from_str(
d.dependency_type.as_str(),
),
}) })
.collect(), .collect(),
game_versions: data.game_versions.into_iter().map(GameVersion).collect(), game_versions: data
.game_versions
.into_iter()
.map(GameVersion)
.collect(),
loaders: data.loaders.into_iter().map(Loader).collect(), loaders: data.loaders.into_iter().map(Loader).collect(),
} }
} }

View File

@@ -34,12 +34,20 @@ impl ResponseError for ARError {
reset, reset,
} => { } => {
let mut response = actix_web::HttpResponse::TooManyRequests(); let mut response = actix_web::HttpResponse::TooManyRequests();
response.insert_header(("x-ratelimit-limit", max_requests.to_string())); response.insert_header((
response.insert_header(("x-ratelimit-remaining", remaining.to_string())); "x-ratelimit-limit",
response.insert_header(("x-ratelimit-reset", reset.to_string())); max_requests.to_string(),
));
response.insert_header((
"x-ratelimit-remaining",
remaining.to_string(),
));
response
.insert_header(("x-ratelimit-reset", reset.to_string()));
response.body(self.to_string()) response.body(self.to_string())
} }
_ => actix_web::HttpResponse::build(self.status_code()).body(self.to_string()), _ => actix_web::HttpResponse::build(self.status_code())
.body(self.to_string()),
} }
} }
} }

View File

@@ -36,9 +36,9 @@ impl MemoryStore {
pub fn with_capacity(capacity: usize) -> Self { pub fn with_capacity(capacity: usize) -> Self {
debug!("Creating new MemoryStore"); debug!("Creating new MemoryStore");
MemoryStore { MemoryStore {
inner: Arc::new(DashMap::<String, (usize, Duration)>::with_capacity( inner: Arc::new(
capacity, DashMap::<String, (usize, Duration)>::with_capacity(capacity),
)), ),
} }
} }
} }
@@ -74,10 +74,18 @@ impl Supervised for MemoryStoreActor {
impl Handler<ActorMessage> for MemoryStoreActor { impl Handler<ActorMessage> for MemoryStoreActor {
type Result = ActorResponse; type Result = ActorResponse;
fn handle(&mut self, msg: ActorMessage, ctx: &mut Self::Context) -> Self::Result { fn handle(
&mut self,
msg: ActorMessage,
ctx: &mut Self::Context,
) -> Self::Result {
match msg { match msg {
ActorMessage::Set { key, value, expiry } => { ActorMessage::Set { key, value, expiry } => {
debug!("Inserting key {} with expiry {}", &key, &expiry.as_secs()); debug!(
"Inserting key {} with expiry {}",
&key,
&expiry.as_secs()
);
let future_key = String::from(&key); let future_key = String::from(&key);
let now = SystemTime::now(); let now = SystemTime::now();
let now = now.duration_since(UNIX_EPOCH).unwrap(); let now = now.duration_since(UNIX_EPOCH).unwrap();
@@ -85,7 +93,10 @@ impl Handler<ActorMessage> for MemoryStoreActor {
ctx.notify_later(ActorMessage::Remove(future_key), expiry); ctx.notify_later(ActorMessage::Remove(future_key), expiry);
ActorResponse::Set(Box::pin(future::ready(Ok(())))) ActorResponse::Set(Box::pin(future::ready(Ok(()))))
} }
ActorMessage::Update { key, value } => match self.inner.get_mut(&key) { ActorMessage::Update { key, value } => match self
.inner
.get_mut(&key)
{
Some(mut c) => { Some(mut c) => {
let val_mut: &mut (usize, Duration) = c.value_mut(); let val_mut: &mut (usize, Duration) = c.value_mut();
if val_mut.0 > value { if val_mut.0 > value {
@@ -98,7 +109,9 @@ impl Handler<ActorMessage> for MemoryStoreActor {
} }
None => { None => {
return ActorResponse::Update(Box::pin(future::ready(Err( return ActorResponse::Update(Box::pin(future::ready(Err(
ARError::ReadWriteError("memory store: read failed!".to_string()), ARError::ReadWriteError(
"memory store: read failed!".to_string(),
),
)))) ))))
} }
}, },
@@ -107,9 +120,11 @@ impl Handler<ActorMessage> for MemoryStoreActor {
let val = match self.inner.get(&key) { let val = match self.inner.get(&key) {
Some(c) => c, Some(c) => c,
None => { None => {
return ActorResponse::Get(Box::pin(future::ready(Err( return ActorResponse::Get(Box::pin(future::ready(
ARError::ReadWriteError("memory store: read failed!".to_string()), Err(ARError::ReadWriteError(
)))) "memory store: read failed!".to_string(),
)),
)))
} }
}; };
let val = val.value().0; let val = val.value().0;
@@ -122,14 +137,17 @@ impl Handler<ActorMessage> for MemoryStoreActor {
let c = match self.inner.get(&key) { let c = match self.inner.get(&key) {
Some(d) => d, Some(d) => d,
None => { None => {
return ActorResponse::Expire(Box::pin(future::ready(Err( return ActorResponse::Expire(Box::pin(future::ready(
ARError::ReadWriteError("memory store: read failed!".to_string()), Err(ARError::ReadWriteError(
)))) "memory store: read failed!".to_string(),
)),
)))
} }
}; };
let dur = c.value().1; let dur = c.value().1;
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let res = dur.checked_sub(now).unwrap_or_else(|| Duration::new(0, 0)); let res =
dur.checked_sub(now).unwrap_or_else(|| Duration::new(0, 0));
ActorResponse::Expire(Box::pin(future::ready(Ok(res)))) ActorResponse::Expire(Box::pin(future::ready(Ok(res))))
} }
ActorMessage::Remove(key) => { ActorMessage::Remove(key) => {
@@ -137,9 +155,11 @@ impl Handler<ActorMessage> for MemoryStoreActor {
let val = match self.inner.remove::<String>(&key) { let val = match self.inner.remove::<String>(&key) {
Some(c) => c, Some(c) => c,
None => { None => {
return ActorResponse::Remove(Box::pin(future::ready(Err( return ActorResponse::Remove(Box::pin(future::ready(
ARError::ReadWriteError("memory store: remove failed!".to_string()), Err(ARError::ReadWriteError(
)))) "memory store: remove failed!".to_string(),
)),
)))
} }
}; };
let val = val.1; let val = val.1;

View File

@@ -95,7 +95,9 @@ where
} }
/// Function to get the identifier for the client request /// Function to get the identifier for the client request
pub fn with_identifier<F: Fn(&ServiceRequest) -> Result<String, ARError> + 'static>( pub fn with_identifier<
F: Fn(&ServiceRequest) -> Result<String, ARError> + 'static,
>(
mut self, mut self,
identifier: F, identifier: F,
) -> Self { ) -> Self {
@@ -108,7 +110,8 @@ impl<T, S, B> Transform<S, ServiceRequest> for RateLimiter<T>
where where
T: Handler<ActorMessage> + Send + Sync + 'static, T: Handler<ActorMessage> + Send + Sync + 'static,
T::Context: ToEnvelope<T, ActorMessage>, T::Context: ToEnvelope<T, ActorMessage>,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError> + 'static, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError>
+ 'static,
S::Future: 'static, S::Future: 'static,
B: 'static, B: 'static,
{ {
@@ -141,23 +144,29 @@ where
// Exists here for the sole purpose of knowing the max_requests and interval from RateLimiter // Exists here for the sole purpose of knowing the max_requests and interval from RateLimiter
max_requests: usize, max_requests: usize,
interval: u64, interval: u64,
identifier: Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError> + 'static>>, identifier:
Rc<Box<dyn Fn(&ServiceRequest) -> Result<String, ARError> + 'static>>,
ignore_ips: Vec<String>, ignore_ips: Vec<String>,
} }
impl<T, S, B> Service<ServiceRequest> for RateLimitMiddleware<S, T> impl<T, S, B> Service<ServiceRequest> for RateLimitMiddleware<S, T>
where where
T: Handler<ActorMessage> + 'static, T: Handler<ActorMessage> + 'static,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError> + 'static, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = AWError>
+ 'static,
S::Future: 'static, S::Future: 'static,
B: 'static, B: 'static,
T::Context: ToEnvelope<T, ActorMessage>, T::Context: ToEnvelope<T, ActorMessage>,
{ {
type Response = ServiceResponse<B>; type Response = ServiceResponse<B>;
type Error = S::Error; type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>; type Future =
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(
&self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.service.borrow_mut().poll_ready(cx) self.service.borrow_mut().poll_ready(cx)
} }
@@ -185,9 +194,15 @@ where
if let Some(c) = opt { if let Some(c) = opt {
// Existing entry in store // Existing entry in store
let expiry = store let expiry = store
.send(ActorMessage::Expire(String::from(&identifier))) .send(ActorMessage::Expire(String::from(
&identifier,
)))
.await .await
.map_err(|_| ARError::ReadWriteError("Setting timeout".to_string()))?; .map_err(|_| {
ARError::ReadWriteError(
"Setting timeout".to_string(),
)
})?;
let reset: Duration = match expiry { let reset: Duration = match expiry {
ActorResponse::Expire(dur) => dur.await?, ActorResponse::Expire(dur) => dur.await?,
_ => unreachable!(), _ => unreachable!(),
@@ -209,7 +224,9 @@ where
}) })
.await .await
.map_err(|_| { .map_err(|_| {
ARError::ReadWriteError("Decrementing ratelimit".to_string()) ARError::ReadWriteError(
"Decrementing ratelimit".to_string(),
)
})?; })?;
let updated_value: usize = match res { let updated_value: usize = match res {
ActorResponse::Update(c) => c.await?, ActorResponse::Update(c) => c.await?,
@@ -222,15 +239,23 @@ where
// Safe unwraps, since usize is always convertible to string // Safe unwraps, since usize is always convertible to string
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-limit"), HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_str(max_requests.to_string().as_str())?, HeaderValue::from_str(
max_requests.to_string().as_str(),
)?,
); );
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-remaining"), HeaderName::from_static(
HeaderValue::from_str(updated_value.to_string().as_str())?, "x-ratelimit-remaining",
),
HeaderValue::from_str(
updated_value.to_string().as_str(),
)?,
); );
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-reset"), HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(reset.as_secs().to_string().as_str())?, HeaderValue::from_str(
reset.as_secs().to_string().as_str(),
)?,
); );
Ok(res) Ok(res)
} }
@@ -245,7 +270,9 @@ where
}) })
.await .await
.map_err(|_| { .map_err(|_| {
ARError::ReadWriteError("Creating store entry".to_string()) ARError::ReadWriteError(
"Creating store entry".to_string(),
)
})?; })?;
match res { match res {
ActorResponse::Set(c) => c.await?, ActorResponse::Set(c) => c.await?,
@@ -257,15 +284,24 @@ where
// Safe unwraps, since usize is always convertible to string // Safe unwraps, since usize is always convertible to string
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-limit"), HeaderName::from_static("x-ratelimit-limit"),
HeaderValue::from_str(max_requests.to_string().as_str()).unwrap(), HeaderValue::from_str(
max_requests.to_string().as_str(),
)
.unwrap(),
); );
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-remaining"), HeaderName::from_static("x-ratelimit-remaining"),
HeaderValue::from_str(current_value.to_string().as_str()).unwrap(), HeaderValue::from_str(
current_value.to_string().as_str(),
)
.unwrap(),
); );
headers.insert( headers.insert(
HeaderName::from_static("x-ratelimit-reset"), HeaderName::from_static("x-ratelimit-reset"),
HeaderValue::from_str(interval.as_secs().to_string().as_str()).unwrap(), HeaderValue::from_str(
interval.as_secs().to_string().as_str(),
)
.unwrap(),
); );
Ok(res) Ok(res)
} }

View File

@@ -38,14 +38,26 @@ pub enum AuthorizationError {
impl actix_web::ResponseError for AuthorizationError { impl actix_web::ResponseError for AuthorizationError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
AuthorizationError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR, AuthorizationError::EnvError(..) => {
AuthorizationError::SqlxDatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR
AuthorizationError::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR, }
AuthorizationError::SqlxDatabaseError(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthorizationError::DatabaseError(..) => {
StatusCode::INTERNAL_SERVER_ERROR
}
AuthorizationError::SerDeError(..) => StatusCode::BAD_REQUEST, AuthorizationError::SerDeError(..) => StatusCode::BAD_REQUEST,
AuthorizationError::GithubError(..) => StatusCode::FAILED_DEPENDENCY, AuthorizationError::GithubError(..) => {
AuthorizationError::InvalidCredentialsError => StatusCode::UNAUTHORIZED, StatusCode::FAILED_DEPENDENCY
}
AuthorizationError::InvalidCredentialsError => {
StatusCode::UNAUTHORIZED
}
AuthorizationError::DecodingError(..) => StatusCode::BAD_REQUEST, AuthorizationError::DecodingError(..) => StatusCode::BAD_REQUEST,
AuthorizationError::AuthenticationError(..) => StatusCode::UNAUTHORIZED, AuthorizationError::AuthenticationError(..) => {
StatusCode::UNAUTHORIZED
}
} }
} }
@@ -57,9 +69,13 @@ impl actix_web::ResponseError for AuthorizationError {
AuthorizationError::DatabaseError(..) => "database_error", AuthorizationError::DatabaseError(..) => "database_error",
AuthorizationError::SerDeError(..) => "invalid_input", AuthorizationError::SerDeError(..) => "invalid_input",
AuthorizationError::GithubError(..) => "github_error", AuthorizationError::GithubError(..) => "github_error",
AuthorizationError::InvalidCredentialsError => "invalid_credentials", AuthorizationError::InvalidCredentialsError => {
"invalid_credentials"
}
AuthorizationError::DecodingError(..) => "decoding_error", AuthorizationError::DecodingError(..) => "decoding_error",
AuthorizationError::AuthenticationError(..) => "authentication_error", AuthorizationError::AuthenticationError(..) => {
"authentication_error"
}
}, },
description: &self.to_string(), description: &self.to_string(),
}) })
@@ -174,11 +190,14 @@ pub async fn auth_callback(
let user = get_github_user_from_token(&*token.access_token).await?; 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 { match user_result {
Some(_) => {} Some(_) => {}
None => { None => {
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_increment: i32 = 0;
let mut username = None; let mut username = None;

View File

@@ -58,7 +58,11 @@ pub async fn maven_metadata(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let project_id = params.into_inner().0; let project_id = params.into_inner().0;
let project_data = 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 data = if let Some(data) = project_data { let data = if let Some(data) = project_data {
data data
@@ -119,7 +123,9 @@ fn find_file<'a>(
version: &'a QueryVersion, version: &'a QueryVersion,
file: &str, file: &str,
) -> Option<&'a QueryFile> { ) -> 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); return Some(selected_file);
} }
@@ -129,7 +135,9 @@ fn find_file<'a>(
_ => return None, _ => return None,
}; };
if file == format!("{}-{}.{}", &project_id, &version.version_number, fileext) { if file
== format!("{}-{}.{}", &project_id, &version.version_number, fileext)
{
version version
.files .files
.iter() .iter()
@@ -148,7 +156,11 @@ pub async fn version_file(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner(); let (project_id, vnum, file) = params.into_inner();
let project_data = 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 { let project = if let Some(data) = project_data {
data data
@@ -175,9 +187,11 @@ pub async fn version_file(
return Ok(HttpResponse::NotFound().body("")); return Ok(HttpResponse::NotFound().body(""));
}; };
let version = if let Some(version) = let version = if let Some(version) = database::models::Version::get_full(
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool) database::models::ids::VersionId(vid.id),
.await? &**pool,
)
.await?
{ {
version version
} else { } else {
@@ -197,10 +211,12 @@ pub async fn version_file(
name: project.inner.title, name: project.inner.title,
description: project.inner.description, description: project.inner.description,
}; };
return Ok(HttpResponse::Ok() return Ok(HttpResponse::Ok().content_type("text/xml").body(
.content_type("text/xml") yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?,
.body(yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?)); ));
} 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() return Ok(HttpResponse::TemporaryRedirect()
.append_header(("location", &*selected_file.url)) .append_header(("location", &*selected_file.url))
.body("")); .body(""));
@@ -217,7 +233,11 @@ pub async fn version_file_sha1(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner(); let (project_id, vnum, file) = params.into_inner();
let project_data = 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 { let project = if let Some(data) = project_data {
data data
@@ -244,9 +264,11 @@ pub async fn version_file_sha1(
return Ok(HttpResponse::NotFound().body("")); return Ok(HttpResponse::NotFound().body(""));
}; };
let version = if let Some(version) = let version = if let Some(version) = database::models::Version::get_full(
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool) database::models::ids::VersionId(vid.id),
.await? &**pool,
)
.await?
{ {
version version
} else { } else {
@@ -268,7 +290,11 @@ pub async fn version_file_sha512(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner(); let (project_id, vnum, file) = params.into_inner();
let project_data = 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 { let project = if let Some(data) = project_data {
data data
@@ -295,9 +321,11 @@ pub async fn version_file_sha512(
return Ok(HttpResponse::NotFound().body("")); return Ok(HttpResponse::NotFound().body(""));
}; };
let version = if let Some(version) = let version = if let Some(version) = database::models::Version::get_full(
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool) database::models::ids::VersionId(vid.id),
.await? &**pool,
)
.await?
{ {
version version
} else { } else {

View File

@@ -187,38 +187,62 @@ pub enum ApiError {
impl actix_web::ResponseError for ApiError { impl actix_web::ResponseError for ApiError {
fn status_code(&self) -> actix_web::http::StatusCode { fn status_code(&self) -> actix_web::http::StatusCode {
match self { match self {
ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::EnvError(..) => {
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
ApiError::SqlxDatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, }
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED, ApiError::DatabaseError(..) => {
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED, actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
ApiError::XmlError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, }
ApiError::SqlxDatabaseError(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::AuthenticationError(..) => {
actix_web::http::StatusCode::UNAUTHORIZED
}
ApiError::CustomAuthenticationError(..) => {
actix_web::http::StatusCode::UNAUTHORIZED
}
ApiError::XmlError(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::JsonError(..) => actix_web::http::StatusCode::BAD_REQUEST, ApiError::JsonError(..) => actix_web::http::StatusCode::BAD_REQUEST,
ApiError::SearchError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::SearchError(..) => {
ApiError::IndexingError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
ApiError::FileHostingError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, }
ApiError::InvalidInputError(..) => actix_web::http::StatusCode::BAD_REQUEST, ApiError::IndexingError(..) => {
ApiError::ValidationError(..) => actix_web::http::StatusCode::BAD_REQUEST, actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::FileHostingError(..) => {
actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
}
ApiError::InvalidInputError(..) => {
actix_web::http::StatusCode::BAD_REQUEST
}
ApiError::ValidationError(..) => {
actix_web::http::StatusCode::BAD_REQUEST
}
} }
} }
fn error_response(&self) -> actix_web::HttpResponse { fn error_response(&self) -> actix_web::HttpResponse {
actix_web::HttpResponse::build(self.status_code()).json(crate::models::error::ApiError { actix_web::HttpResponse::build(self.status_code()).json(
error: match self { crate::models::error::ApiError {
ApiError::EnvError(..) => "environment_error", error: match self {
ApiError::SqlxDatabaseError(..) => "database_error", ApiError::EnvError(..) => "environment_error",
ApiError::DatabaseError(..) => "database_error", ApiError::SqlxDatabaseError(..) => "database_error",
ApiError::AuthenticationError(..) => "unauthorized", ApiError::DatabaseError(..) => "database_error",
ApiError::CustomAuthenticationError(..) => "unauthorized", ApiError::AuthenticationError(..) => "unauthorized",
ApiError::XmlError(..) => "xml_error", ApiError::CustomAuthenticationError(..) => "unauthorized",
ApiError::JsonError(..) => "json_error", ApiError::XmlError(..) => "xml_error",
ApiError::SearchError(..) => "search_error", ApiError::JsonError(..) => "json_error",
ApiError::IndexingError(..) => "indexing_error", ApiError::SearchError(..) => "search_error",
ApiError::FileHostingError(..) => "file_hosting_error", ApiError::IndexingError(..) => "indexing_error",
ApiError::InvalidInputError(..) => "invalid_input", ApiError::FileHostingError(..) => "file_hosting_error",
ApiError::ValidationError(..) => "invalid_input", ApiError::InvalidInputError(..) => "invalid_input",
ApiError::ValidationError(..) => "invalid_input",
},
description: &self.to_string(),
}, },
description: &self.to_string(), )
})
} }
} }

View File

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

View File

@@ -31,8 +31,11 @@ pub async fn notifications_get(
.collect(); .collect();
let notifications_data: Vec<DBNotification> = let notifications_data: Vec<DBNotification> =
database::models::notification_item::Notification::get_many(notification_ids, &**pool) database::models::notification_item::Notification::get_many(
.await?; notification_ids,
&**pool,
)
.await?;
let notifications: Vec<Notification> = notifications_data let notifications: Vec<Notification> = notifications_data
.into_iter() .into_iter()
@@ -54,7 +57,11 @@ pub async fn notification_get(
let id = info.into_inner().0; let id = info.into_inner().0;
let notification_data = 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 let Some(data) = notification_data {
if user.id == data.user_id.into() || user.role.is_mod() { if user.id == data.user_id.into() || user.role.is_mod() {
@@ -78,21 +85,29 @@ pub async fn notification_delete(
let id = info.into_inner().0; let id = info.into_inner().0;
let notification_data = 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 let Some(data) = notification_data {
if data.user_id == user.id.into() || user.role.is_mod() { if data.user_id == user.id.into() || user.role.is_mod() {
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
database::models::notification_item::Notification::remove(id.into(), &mut transaction) database::models::notification_item::Notification::remove(
.await?; id.into(),
&mut transaction,
)
.await?;
transaction.commit().await?; transaction.commit().await?;
Ok(HttpResponse::NoContent().body("")) Ok(HttpResponse::NoContent().body(""))
} else { } else {
Err(ApiError::CustomAuthenticationError( Err(ApiError::CustomAuthenticationError(
"You are not authorized to delete this notification!".to_string(), "You are not authorized to delete this notification!"
.to_string(),
)) ))
} }
} else { } else {
@@ -108,18 +123,23 @@ pub async fn notifications_delete(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&*ids.ids)? let notification_ids =
.into_iter() serde_json::from_str::<Vec<NotificationId>>(&*ids.ids)?
.map(|x| x.into()) .into_iter()
.collect(); .map(|x| x.into())
.collect();
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let notifications_data = let notifications_data =
database::models::notification_item::Notification::get_many(notification_ids, &**pool) database::models::notification_item::Notification::get_many(
.await?; 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 { for notification in notifications_data {
if notification.user_id == user.id.into() || user.role.is_mod() { if notification.user_id == user.id.into() || user.role.is_mod() {
@@ -127,8 +147,11 @@ pub async fn notifications_delete(
} }
} }
database::models::notification_item::Notification::remove_many(notifications, &mut transaction) database::models::notification_item::Notification::remove_many(
.await?; notifications,
&mut transaction,
)
.await?;
transaction.commit().await?; transaction.commit().await?;

View File

@@ -67,10 +67,14 @@ impl actix_web::ResponseError for CreateError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self { match self {
CreateError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR, 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::DatabaseError(..) => StatusCode::INTERNAL_SERVER_ERROR,
CreateError::IndexingError(..) => 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::SerDeError(..) => StatusCode::BAD_REQUEST,
CreateError::MultipartError(..) => StatusCode::BAD_REQUEST, CreateError::MultipartError(..) => StatusCode::BAD_REQUEST,
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST, CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
@@ -81,7 +85,9 @@ impl actix_web::ResponseError for CreateError {
CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST, CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST,
CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST, CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST,
CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED, CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED,
CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED, CreateError::CustomAuthenticationError(..) => {
StatusCode::UNAUTHORIZED
}
CreateError::SlugCollision => StatusCode::BAD_REQUEST, CreateError::SlugCollision => StatusCode::BAD_REQUEST,
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST, CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST, CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
@@ -297,17 +303,21 @@ pub async fn project_create_inner(
let cdn_url = dotenv::var("CDN_URL")?; let cdn_url = dotenv::var("CDN_URL")?;
// The currently logged in user // 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 project_create_data;
let mut versions; let mut versions;
let mut versions_map = std::collections::HashMap::new(); let mut versions_map = std::collections::HashMap::new();
let mut gallery_urls = Vec::new(); let mut gallery_urls = Vec::new();
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; let all_game_versions =
let all_loaders = models::categories::Loader::list(&mut *transaction).await?; 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 // The first multipart field must be named "data" and contain a
@@ -324,9 +334,9 @@ pub async fn project_create_inner(
})?; })?;
let content_disposition = field.content_disposition(); let content_disposition = field.content_disposition();
let name = content_disposition let name = content_disposition.get_name().ok_or_else(|| {
.get_name() CreateError::MissingValueError(String::from("Missing content name"))
.ok_or_else(|| CreateError::MissingValueError(String::from("Missing content name")))?; })?;
if name != "data" { if name != "data" {
return Err(CreateError::InvalidInput(String::from( return Err(CreateError::InvalidInput(String::from(
@@ -336,19 +346,22 @@ pub async fn project_create_inner(
let mut data = Vec::new(); let mut data = Vec::new();
while let Some(chunk) = field.next().await { 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)?; let create_data: ProjectCreateData = serde_json::from_slice(&data)?;
create_data create_data.validate().map_err(|err| {
.validate() CreateError::InvalidInput(validation_errors_to_string(err, None))
.map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?; })?;
let slug_project_id_option: Option<ProjectId> = let slug_project_id_option: Option<ProjectId> =
serde_json::from_str(&*format!("\"{}\"", create_data.slug)).ok(); serde_json::from_str(&*format!("\"{}\"", create_data.slug)).ok();
if let Some(slug_project_id) = slug_project_id_option { 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!( let results = sqlx::query!(
" "
SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1) SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)
@@ -393,15 +406,17 @@ pub async fn project_create_inner(
project_create_data = create_data; project_create_data = create_data;
} }
let project_type_id = let project_type_id = models::ProjectTypeId::get_id(
models::ProjectTypeId::get_id(project_create_data.project_type.clone(), &mut *transaction) project_create_data.project_type.clone(),
.await? &mut *transaction,
.ok_or_else(|| { )
CreateError::InvalidInput(format!( .await?
"Project Type {} does not exist.", .ok_or_else(|| {
project_create_data.project_type.clone() CreateError::InvalidInput(format!(
)) "Project Type {} does not exist.",
})?; project_create_data.project_type.clone()
))
})?;
let mut icon_url = None; let mut icon_url = None;
@@ -409,9 +424,9 @@ pub async fn project_create_inner(
let mut field: Field = item.map_err(CreateError::MultipartError)?; let mut field: Field = item.map_err(CreateError::MultipartError)?;
let content_disposition = field.content_disposition().clone(); let content_disposition = field.content_disposition().clone();
let name = content_disposition let name = content_disposition.get_name().ok_or_else(|| {
.get_name() CreateError::MissingValueError("Missing content name".to_string())
.ok_or_else(|| CreateError::MissingValueError("Missing content name".to_string()))?; })?;
let (file_name, file_extension) = let (file_name, file_extension) =
super::version_creation::get_name_ext(&content_disposition)?; super::version_creation::get_name_ext(&content_disposition)?;
@@ -454,11 +469,21 @@ pub async fn project_create_inner(
let hash = sha1::Sha1::from(&data).hexdigest(); let hash = sha1::Sha1::from(&data).hexdigest();
let (_, file_extension) = let (_, file_extension) =
super::version_creation::get_name_ext(&content_disposition)?; super::version_creation::get_name_ext(
let content_type = crate::util::ext::get_image_content_type(file_extension) &content_disposition,
.ok_or_else(|| CreateError::InvalidIconFormat(file_extension.to_string()))?; )?;
let content_type =
crate::util::ext::get_image_content_type(file_extension)
.ok_or_else(|| {
CreateError::InvalidIconFormat(
file_extension.to_string(),
)
})?;
let url = format!("data/{}/images/{}.{}", project_id, hash, file_extension); let url = format!(
"data/{}/images/{}.{}",
project_id, hash, file_extension
);
let upload_data = file_host let upload_data = file_host
.upload_file(content_type, &url, data.freeze()) .upload_file(content_type, &url, data.freeze())
.await?; .await?;
@@ -491,7 +516,8 @@ pub async fn project_create_inner(
// `index` is always valid for these lists // `index` is always valid for these lists
let created_version = versions.get_mut(index).unwrap(); 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 // Upload the new jar file
super::version_creation::upload_file( super::version_creation::upload_file(
@@ -529,7 +555,8 @@ pub async fn project_create_inner(
} }
// Convert the list of category names to actual categories // 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 { for category in &project_create_data.categories {
let id = models::categories::Category::get_id_project( let id = models::categories::Category::get_id_project(
category, category,
@@ -568,44 +595,58 @@ pub async fn project_create_inner(
let status_id = models::StatusId::get_id(&status, &mut *transaction) let status_id = models::StatusId::get_id(&status, &mut *transaction)
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
CreateError::InvalidInput(format!("Status {} does not exist.", status.clone())) CreateError::InvalidInput(format!(
"Status {} does not exist.",
status.clone()
))
})?; })?;
let client_side_id = let client_side_id = models::SideTypeId::get_id(
models::SideTypeId::get_id(&project_create_data.client_side, &mut *transaction) &project_create_data.client_side,
.await? &mut *transaction,
.ok_or_else(|| { )
CreateError::InvalidInput( .await?
"Client side type specified does not exist.".to_string(), .ok_or_else(|| {
) CreateError::InvalidInput(
})?; "Client side type specified does not exist.".to_string(),
)
})?;
let server_side_id = let server_side_id = models::SideTypeId::get_id(
models::SideTypeId::get_id(&project_create_data.server_side, &mut *transaction) &project_create_data.server_side,
.await? &mut *transaction,
.ok_or_else(|| { )
CreateError::InvalidInput( .await?
"Server side type specified does not exist.".to_string(), .ok_or_else(|| {
) CreateError::InvalidInput(
})?; "Server side type specified does not exist.".to_string(),
)
})?;
let license_id = let license_id = models::categories::License::get_id(
models::categories::License::get_id(&project_create_data.license_id, &mut *transaction) &project_create_data.license_id,
.await? &mut *transaction,
.ok_or_else(|| { )
CreateError::InvalidInput("License specified does not exist.".to_string()) .await?
})?; .ok_or_else(|| {
CreateError::InvalidInput(
"License specified does not exist.".to_string(),
)
})?;
let mut donation_urls = vec![]; let mut donation_urls = vec![];
if let Some(urls) = &project_create_data.donation_urls { if let Some(urls) = &project_create_data.donation_urls {
for url in urls { for url in urls {
let platform_id = models::DonationPlatformId::get_id(&url.id, &mut *transaction) let platform_id = models::DonationPlatformId::get_id(
.await? &url.id,
.ok_or_else(|| { &mut *transaction,
CreateError::InvalidInput(format!( )
"Donation platform {} does not exist.", .await?
url.id.clone() .ok_or_else(|| {
)) CreateError::InvalidInput(format!(
})?; "Donation platform {} does not exist.",
url.id.clone()
))
})?;
donation_urls.push(models::project_item::DonationUrl { donation_urls.push(models::project_item::DonationUrl {
project_id: project_id.into(), project_id: project_id.into(),
@@ -695,9 +736,12 @@ pub async fn project_create_inner(
if status == ProjectStatus::Processing { if status == ProjectStatus::Processing {
if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") { if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") {
crate::util::webhook::send_discord_webhook(response.clone(), webhook_url) crate::util::webhook::send_discord_webhook(
.await response.clone(),
.ok(); webhook_url,
)
.await
.ok();
} }
} }
@@ -720,12 +764,13 @@ async fn create_initial_version(
))); )));
} }
version_data version_data.validate().map_err(|err| {
.validate() CreateError::ValidationError(validation_errors_to_string(err, None))
.map_err(|err| CreateError::ValidationError(validation_errors_to_string(err, None)))?; })?;
// Randomly generate a new id to be used for the version // 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 let game_versions = version_data
.game_versions .game_versions
@@ -794,8 +839,15 @@ async fn process_icon_upload(
mut field: actix_multipart::Field, mut field: actix_multipart::Field,
cdn_url: &str, cdn_url: &str,
) -> Result<String, CreateError> { ) -> Result<String, CreateError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) { if let Some(content_type) =
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?; 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 upload_data = file_host let upload_data = file_host
.upload_file( .upload_file(

View File

@@ -39,12 +39,14 @@ pub async fn projects_get(
web::Query(ids): web::Query<ProjectIds>, web::Query(ids): web::Query<ProjectIds>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let project_ids = serde_json::from_str::<Vec<models::ids::ProjectId>>(&*ids.ids)? let project_ids =
.into_iter() serde_json::from_str::<Vec<models::ids::ProjectId>>(&*ids.ids)?
.map(|x| x.into()) .into_iter()
.collect(); .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 user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -71,7 +73,10 @@ pub async fn project_get(
let string = info.into_inner().0; let string = info.into_inner().0;
let project_data = 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(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -96,7 +101,9 @@ pub async fn dependency_list(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0; 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) = result { if let Some(project) = result {
let id = project.id; let id = project.id;
@@ -105,9 +112,10 @@ pub async fn dependency_list(
let dependencies = sqlx::query!( let dependencies = sqlx::query!(
" "
SELECT d.dependent_id, d.dependency_id, d.mod_dependency_id SELECT d.dependency_id, vd.mod_id
FROM versions v FROM versions v
INNER JOIN dependencies d ON d.dependent_id = v.id INNER JOIN dependencies d ON d.dependent_id = v.id
INNER JOIN versions vd ON d.dependency_id = vd.id
WHERE v.mod_id = $1 WHERE v.mod_id = $1
", ",
id as database::models::ProjectId id as database::models::ProjectId
@@ -116,26 +124,24 @@ pub async fn dependency_list(
.try_filter_map(|e| async { .try_filter_map(|e| async {
Ok(e.right().map(|x| { Ok(e.right().map(|x| {
( (
database::models::VersionId(x.dependent_id),
x.dependency_id.map(database::models::VersionId), x.dependency_id.map(database::models::VersionId),
x.mod_dependency_id.map(database::models::ProjectId), database::models::ProjectId(x.mod_id),
) )
})) }))
}) })
.try_collect::<Vec<( .try_collect::<Vec<(
database::models::VersionId,
Option<database::models::VersionId>, Option<database::models::VersionId>,
Option<database::models::ProjectId>, database::models::ProjectId,
)>>() )>>()
.await?; .await?;
let (projects_result, versions_result) = futures::join!( let (projects_result, versions_result) = futures::join!(
database::Project::get_many_full( database::Project::get_many_full(
dependencies.iter().map(|x| x.2).flatten().collect(), dependencies.iter().map(|x| x.1).collect(),
&**pool, &**pool,
), ),
database::Version::get_many_full( database::Version::get_many_full(
dependencies.iter().map(|x| x.1).flatten().collect(), dependencies.iter().map(|x| x.0).flatten().collect(),
&**pool, &**pool,
) )
); );
@@ -244,13 +250,15 @@ pub async fn project_edit(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
new_project new_project.validate().map_err(|err| {
.validate() ApiError::ValidationError(validation_errors_to_string(err, None))
.map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?; })?;
let string = info.into_inner().0; let string = info.into_inner().0;
let result = let result = database::models::Project::get_full_from_slug_or_project_id(
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool).await?; &string, &**pool,
)
.await?;
if let Some(project_item) = result { if let Some(project_item) = result {
let id = project_item.inner.id; let id = project_item.inner.id;
@@ -324,11 +332,13 @@ pub async fn project_edit(
)); ));
} }
if (status == &ProjectStatus::Rejected || status == &ProjectStatus::Approved) if (status == &ProjectStatus::Rejected
|| status == &ProjectStatus::Approved)
&& !user.role.is_mod() && !user.role.is_mod()
{ {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"You don't have permission to set this status".to_string(), "You don't have permission to set this status"
.to_string(),
)); ));
} }
@@ -361,7 +371,9 @@ pub async fn project_edit(
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") { if let Ok(webhook_url) =
dotenv::var("MODERATION_DISCORD_WEBHOOK")
{
crate::util::webhook::send_discord_webhook( crate::util::webhook::send_discord_webhook(
Project::from(project_item.clone()), Project::from(project_item.clone()),
webhook_url, webhook_url,
@@ -371,13 +383,16 @@ pub async fn project_edit(
} }
} }
let status_id = database::models::StatusId::get_id(status, &mut *transaction) let status_id = database::models::StatusId::get_id(
.await? status,
.ok_or_else(|| { &mut *transaction,
ApiError::InvalidInputError( )
"No database entry for status provided.".to_string(), .await?
) .ok_or_else(|| {
})?; ApiError::InvalidInputError(
"No database entry for status provided.".to_string(),
)
})?;
sqlx::query!( sqlx::query!(
" "
@@ -391,12 +406,19 @@ pub async fn project_edit(
.execute(&mut *transaction) .execute(&mut *transaction)
.await?; .await?;
if project_item.status.is_searchable() && !status.is_searchable() { if project_item.status.is_searchable()
&& !status.is_searchable()
{
delete_from_index(id.into(), config).await?; delete_from_index(id.into(), config).await?;
} else if !project_item.status.is_searchable() && status.is_searchable() { } else if !project_item.status.is_searchable()
&& status.is_searchable()
{
let index_project = let index_project =
crate::search::indexing::local_import::query_one(id, &mut *transaction) crate::search::indexing::local_import::query_one(
.await?; id,
&mut *transaction,
)
.await?;
indexing_queue.add(index_project); indexing_queue.add(index_project);
} }
@@ -422,14 +444,17 @@ pub async fn project_edit(
for category in categories { for category in categories {
let category_id = let category_id =
database::models::categories::Category::get_id(category, &mut *transaction) database::models::categories::Category::get_id(
.await? category,
.ok_or_else(|| { &mut *transaction,
ApiError::InvalidInputError(format!( )
"Category {} does not exist.", .await?
category.clone() .ok_or_else(|| {
)) ApiError::InvalidInputError(format!(
})?; "Category {} does not exist.",
category.clone()
))
})?;
sqlx::query!( sqlx::query!(
" "
@@ -574,7 +599,8 @@ pub async fn project_edit(
if results.exists.unwrap_or(true) { if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInputError( return Err(ApiError::InvalidInputError(
"Slug collides with other project's id!".to_string(), "Slug collides with other project's id!"
.to_string(),
)); ));
} }
} }
@@ -601,10 +627,12 @@ pub async fn project_edit(
)); ));
} }
let side_type_id = let side_type_id = database::models::SideTypeId::get_id(
database::models::SideTypeId::get_id(new_side, &mut *transaction) new_side,
.await? &mut *transaction,
.expect("No database entry found for side type"); )
.await?
.expect("No database entry found for side type");
sqlx::query!( sqlx::query!(
" "
@@ -627,10 +655,12 @@ pub async fn project_edit(
)); ));
} }
let side_type_id = let side_type_id = database::models::SideTypeId::get_id(
database::models::SideTypeId::get_id(new_side, &mut *transaction) new_side,
.await? &mut *transaction,
.expect("No database entry found for side type"); )
.await?
.expect("No database entry found for side type");
sqlx::query!( sqlx::query!(
" "
@@ -653,10 +683,12 @@ pub async fn project_edit(
)); ));
} }
let license_id = let license_id = database::models::categories::License::get_id(
database::models::categories::License::get_id(license, &mut *transaction) license,
.await? &mut *transaction,
.expect("No database entry found for license"); )
.await?
.expect("No database entry found for license");
sqlx::query!( sqlx::query!(
" "
@@ -690,17 +722,18 @@ pub async fn project_edit(
.await?; .await?;
for donation in donations { for donation in donations {
let platform_id = database::models::DonationPlatformId::get_id( let platform_id =
&donation.id, database::models::DonationPlatformId::get_id(
&mut *transaction, &donation.id,
) &mut *transaction,
.await? )
.ok_or_else(|| { .await?
ApiError::InvalidInputError(format!( .ok_or_else(|| {
"Platform {} does not exist.", ApiError::InvalidInputError(format!(
donation.id.clone() "Platform {} does not exist.",
)) donation.id.clone()
})?; ))
})?;
sqlx::query!( sqlx::query!(
" "
@@ -717,7 +750,9 @@ pub async fn project_edit(
} }
if let Some(moderation_message) = &new_project.moderation_message { if let Some(moderation_message) = &new_project.moderation_message {
if !user.role.is_mod() && project_item.status != ProjectStatus::Approved { if !user.role.is_mod()
&& project_item.status != ProjectStatus::Approved
{
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"You do not have the permissions to edit the moderation message of this project!" "You do not have the permissions to edit the moderation message of this project!"
.to_string(), .to_string(),
@@ -737,8 +772,12 @@ pub async fn project_edit(
.await?; .await?;
} }
if let Some(moderation_message_body) = &new_project.moderation_message_body { if let Some(moderation_message_body) =
if !user.role.is_mod() && project_item.status != ProjectStatus::Approved { &new_project.moderation_message_body
{
if !user.role.is_mod()
&& project_item.status != ProjectStatus::Approved
{
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"You do not have the permissions to edit the moderation message body of this project!" "You do not have the permissions to edit the moderation message body of this project!"
.to_string(), .to_string(),
@@ -805,17 +844,24 @@ pub async fn project_icon_edit(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>, file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload, mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> { ) -> 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 = dotenv::var("CDN_URL")?; let cdn_url = dotenv::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project_item = let project_item =
database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) database::models::Project::get_from_slug_or_project_id(
.await? string.clone(),
.ok_or_else(|| { &**pool,
ApiError::InvalidInputError("The specified project does not exist!".to_string()) )
})?; .await?
.ok_or_else(|| {
ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id( let team_member = database::models::TeamMember::get_from_user_id(
@@ -826,12 +872,15 @@ pub async fn project_icon_edit(
.await .await
.map_err(ApiError::DatabaseError)? .map_err(ApiError::DatabaseError)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInputError("The specified project does not exist!".to_string()) ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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(),
)); ));
} }
} }
@@ -844,8 +893,12 @@ pub async fn project_icon_edit(
} }
} }
let bytes = let bytes = read_from_payload(
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?; &mut payload,
262144,
"Icons must be smaller than 256KiB",
)
.await?;
let hash = sha1::Sha1::from(&bytes).hexdigest(); let hash = sha1::Sha1::from(&bytes).hexdigest();
let project_id: ProjectId = project_item.id.into(); let project_id: ProjectId = project_item.id.into();
let upload_data = file_host let upload_data = file_host
@@ -891,12 +944,16 @@ pub async fn delete_project_icon(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project_item = let project_item = database::models::Project::get_from_slug_or_project_id(
database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) string.clone(),
.await? &**pool,
.ok_or_else(|| { )
ApiError::InvalidInputError("The specified project does not exist!".to_string()) .await?
})?; .ok_or_else(|| {
ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id( let team_member = database::models::TeamMember::get_from_user_id(
@@ -907,12 +964,15 @@ pub async fn delete_project_icon(
.await .await
.map_err(ApiError::DatabaseError)? .map_err(ApiError::DatabaseError)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInputError("The specified project does not exist!".to_string()) ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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(),
)); ));
} }
} }
@@ -962,20 +1022,28 @@ pub async fn add_gallery_item(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>, file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload, mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&*ext.ext) { if let Some(content_type) =
item.validate() crate::util::ext::get_image_content_type(&*ext.ext)
.map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?; {
item.validate().map_err(|err| {
ApiError::ValidationError(validation_errors_to_string(err, None))
})?;
let cdn_url = dotenv::var("CDN_URL")?; let cdn_url = dotenv::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project_item = let project_item =
database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) database::models::Project::get_from_slug_or_project_id(
.await? string.clone(),
.ok_or_else(|| { &**pool,
ApiError::InvalidInputError("The specified project does not exist!".to_string()) )
})?; .await?
.ok_or_else(|| {
ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id( let team_member = database::models::TeamMember::get_from_user_id(
@@ -986,12 +1054,15 @@ pub async fn add_gallery_item(
.await .await
.map_err(ApiError::DatabaseError)? .map_err(ApiError::DatabaseError)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInputError("The specified project does not exist!".to_string()) ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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(),
)); ));
} }
} }
@@ -1079,15 +1150,20 @@ pub async fn edit_gallery_item(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
item.validate() item.validate().map_err(|err| {
.map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?; ApiError::ValidationError(validation_errors_to_string(err, None))
})?;
let project_item = let project_item = database::models::Project::get_from_slug_or_project_id(
database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) string.clone(),
.await? &**pool,
.ok_or_else(|| { )
ApiError::InvalidInputError("The specified project does not exist!".to_string()) .await?
})?; .ok_or_else(|| {
ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id( let team_member = database::models::TeamMember::get_from_user_id(
@@ -1098,12 +1174,15 @@ pub async fn edit_gallery_item(
.await .await
.map_err(ApiError::DatabaseError)? .map_err(ApiError::DatabaseError)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInputError("The specified project does not exist!".to_string()) ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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(),
)); ));
} }
} }
@@ -1203,12 +1282,16 @@ pub async fn delete_gallery_item(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project_item = let project_item = database::models::Project::get_from_slug_or_project_id(
database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) string.clone(),
.await? &**pool,
.ok_or_else(|| { )
ApiError::InvalidInputError("The specified project does not exist!".to_string()) .await?
})?; .ok_or_else(|| {
ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id( let team_member = database::models::TeamMember::get_from_user_id(
@@ -1219,12 +1302,15 @@ pub async fn delete_gallery_item(
.await .await
.map_err(ApiError::DatabaseError)? .map_err(ApiError::DatabaseError)?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInputError("The specified project does not exist!".to_string()) ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?; })?;
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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(),
)); ));
} }
} }
@@ -1280,23 +1366,31 @@ pub async fn project_delete(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let project = database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool) let project = database::models::Project::get_from_slug_or_project_id(
.await? string.clone(),
.ok_or_else(|| { &**pool,
ApiError::InvalidInputError("The specified project does not exist!".to_string()) )
})?; .await?
.ok_or_else(|| {
ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id_project( let team_member =
project.id, database::models::TeamMember::get_from_user_id_project(
user.id.into(), project.id,
&**pool, user.id.into(),
) &**pool,
.await )
.map_err(ApiError::DatabaseError)? .await
.ok_or_else(|| { .map_err(ApiError::DatabaseError)?
ApiError::InvalidInputError("The specified project does not exist!".to_string()) .ok_or_else(|| {
})?; ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
if !team_member if !team_member
.permissions .permissions
@@ -1310,7 +1404,9 @@ pub async fn project_delete(
let mut transaction = pool.begin().await?; 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?; transaction.commit().await?;
@@ -1332,11 +1428,14 @@ pub async fn project_follow(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(string, &**pool) let result =
.await? database::models::Project::get_from_slug_or_project_id(string, &**pool)
.ok_or_else(|| { .await?
ApiError::InvalidInputError("The specified project does not exist!".to_string()) .ok_or_else(|| {
})?; ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
let user_id: database::models::ids::UserId = user.id.into(); let user_id: database::models::ids::UserId = user.id.into();
let project_id: database::models::ids::ProjectId = result.id; let project_id: database::models::ids::ProjectId = result.id;
@@ -1397,11 +1496,14 @@ pub async fn project_unfollow(
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0; let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(string, &**pool) let result =
.await? database::models::Project::get_from_slug_or_project_id(string, &**pool)
.ok_or_else(|| { .await?
ApiError::InvalidInputError("The specified project does not exist!".to_string()) .ok_or_else(|| {
})?; ApiError::InvalidInputError(
"The specified project does not exist!".to_string(),
)
})?;
let user_id: database::models::ids::UserId = user.id.into(); let user_id: database::models::ids::UserId = user.id.into();
let project_id = result.id; let project_id = result.id;
@@ -1457,9 +1559,11 @@ pub async fn delete_from_index(
id: crate::models::projects::ProjectId, id: crate::models::projects::ProjectId,
config: web::Data<SearchConfig>, config: web::Data<SearchConfig>,
) -> Result<(), meilisearch_sdk::errors::Error> { ) -> 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: Vec<meilisearch_sdk::indexes::Index> = client.get_indexes().await?; let indexes: Vec<meilisearch_sdk::indexes::Index> =
client.get_indexes().await?;
for index in indexes { for index in indexes {
index.delete_document(format!("{}", id)).await?; index.delete_document(format!("{}", id)).await?;
} }

View File

@@ -1,7 +1,9 @@
use crate::models::ids::{ProjectId, UserId, VersionId}; use crate::models::ids::{ProjectId, UserId, VersionId};
use crate::models::reports::{ItemType, Report}; use crate::models::reports::{ItemType, Report};
use crate::routes::ApiError; 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 actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use futures::StreamExt; use futures::StreamExt;
use serde::Deserialize; use serde::Deserialize;
@@ -23,24 +25,31 @@ pub async fn report_create(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let mut transaction = pool.begin().await?; 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(); let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await { while let Some(item) = body.next().await {
bytes.extend_from_slice(&item.map_err(|_| { bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError("Error while parsing request payload!".to_string()) ApiError::InvalidInputError(
"Error while parsing request payload!".to_string(),
)
})?); })?);
} }
let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?; 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( let report_type = crate::database::models::categories::ReportType::get_id(
&*new_report.report_type, &*new_report.report_type,
&mut *transaction, &mut *transaction,
) )
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInputError(format!("Invalid report type: {}", new_report.report_type)) ApiError::InvalidInputError(format!(
"Invalid report type: {}",
new_report.report_type
))
})?; })?;
let mut report = crate::database::models::report_item::Report { let mut report = crate::database::models::report_item::Report {
id, id,
@@ -56,17 +65,29 @@ pub async fn report_create(
match new_report.item_type { match new_report.item_type {
ItemType::Project => { ItemType::Project => {
report.project_id = Some( report.project_id = Some(
serde_json::from_str::<ProjectId>(&*format!("\"{}\"", new_report.item_id))?.into(), serde_json::from_str::<ProjectId>(&*format!(
"\"{}\"",
new_report.item_id
))?
.into(),
) )
} }
ItemType::Version => { ItemType::Version => {
report.version_id = Some( report.version_id = Some(
serde_json::from_str::<VersionId>(&*format!("\"{}\"", new_report.item_id))?.into(), serde_json::from_str::<VersionId>(&*format!(
"\"{}\"",
new_report.item_id
))?
.into(),
) )
} }
ItemType::User => { ItemType::User => {
report.user_id = Some( report.user_id = Some(
serde_json::from_str::<UserId>(&*format!("\"{}\"", new_report.item_id))?.into(), serde_json::from_str::<UserId>(&*format!(
"\"{}\"",
new_report.item_id
))?
.into(),
) )
} }
ItemType::Unknown => { ItemType::Unknown => {
@@ -127,8 +148,10 @@ pub async fn reports(
.try_collect::<Vec<crate::database::models::ids::ReportId>>() .try_collect::<Vec<crate::database::models::ids::ReportId>>()
.await?; .await?;
let query_reports = let query_reports = crate::database::models::report_item::Report::get_many(
crate::database::models::report_item::Report::get_many(report_ids, &**pool).await?; report_ids, &**pool,
)
.await?;
let mut reports = Vec::new(); let mut reports = Vec::new();

View File

@@ -1,6 +1,8 @@
use super::ApiError; use super::ApiError;
use crate::database::models; use crate::database::models;
use crate::database::models::categories::{DonationPlatform, License, ProjectType, ReportType}; use crate::database::models::categories::{
DonationPlatform, License, ProjectType, ReportType,
};
use crate::util::auth::check_is_admin_from_headers; use crate::util::auth::check_is_admin_from_headers;
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse}; use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
use models::categories::{Category, GameVersion, Loader}; use models::categories::{Category, GameVersion, Loader};
@@ -40,7 +42,9 @@ pub struct CategoryData {
// TODO: searching / filtering? Could be used to implement a live // TODO: searching / filtering? Could be used to implement a live
// searching category list // searching category list
#[get("category")] #[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 mut results = Category::list(&**pool) let mut results = Category::list(&**pool)
.await? .await?
.into_iter() .into_iter()
@@ -64,12 +68,16 @@ pub async fn category_create(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
check_is_admin_from_headers(req.headers(), &**pool).await?; check_is_admin_from_headers(req.headers(), &**pool).await?;
let project_type = let project_type = crate::database::models::ProjectTypeId::get_id(
crate::database::models::ProjectTypeId::get_id(new_category.project_type.clone(), &**pool) new_category.project_type.clone(),
.await? &**pool,
.ok_or_else(|| { )
ApiError::InvalidInputError("Specified project type does not exist!".to_string()) .await?
})?; .ok_or_else(|| {
ApiError::InvalidInputError(
"Specified project type does not exist!".to_string(),
)
})?;
let _id = Category::builder() let _id = Category::builder()
.name(&new_category.name)? .name(&new_category.name)?
@@ -90,7 +98,8 @@ pub async fn category_delete(
check_is_admin_from_headers(req.headers(), &**pool).await?; check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = category.into_inner().0; let name = category.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = Category::remove(&name, &mut transaction).await?; let result = Category::remove(&name, &mut transaction).await?;
@@ -114,7 +123,9 @@ pub struct LoaderData {
} }
#[get("loader")] #[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) let mut results = Loader::list(&**pool)
.await? .await?
.into_iter() .into_iter()
@@ -140,13 +151,18 @@ pub async fn loader_create(
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let project_types = let project_types = ProjectType::get_many_id(
ProjectType::get_many_id(&new_loader.supported_project_types, &mut *transaction).await?; &new_loader.supported_project_types,
&mut *transaction,
)
.await?;
let _id = Loader::builder() let _id = Loader::builder()
.name(&new_loader.name)? .name(&new_loader.name)?
.icon(&new_loader.icon)? .icon(&new_loader.icon)?
.supported_project_types(&*project_types.into_iter().map(|x| x.id).collect::<Vec<_>>())? .supported_project_types(
&*project_types.into_iter().map(|x| x.id).collect::<Vec<_>>(),
)?
.insert(&mut transaction) .insert(&mut transaction)
.await?; .await?;
@@ -164,7 +180,8 @@ pub async fn loader_delete(
check_is_admin_from_headers(req.headers(), &**pool).await?; check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = loader.into_inner().0; let name = loader.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = Loader::remove(&name, &mut transaction).await?; let result = Loader::remove(&name, &mut transaction).await?;
@@ -200,8 +217,11 @@ pub async fn game_version_list(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
query: web::Query<GameVersionQuery>, query: web::Query<GameVersionQuery>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let results: Vec<GameVersionQueryData> = if query.type_.is_some() || query.major.is_some() { let results: Vec<GameVersionQueryData> = if query.type_.is_some()
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await? || query.major.is_some()
{
GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool)
.await?
} else { } else {
GameVersion::list(&**pool).await? GameVersion::list(&**pool).await?
} }
@@ -260,7 +280,8 @@ pub async fn game_version_delete(
check_is_admin_from_headers(req.headers(), &**pool).await?; check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = game_version.into_inner().0; let name = game_version.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = GameVersion::remove(&name, &mut transaction).await?; let result = GameVersion::remove(&name, &mut transaction).await?;
@@ -283,7 +304,9 @@ pub struct LicenseQueryData {
} }
#[get("license")] #[get("license")]
pub async fn license_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> { pub async fn license_list(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let results: Vec<LicenseQueryData> = License::list(&**pool) let results: Vec<LicenseQueryData> = License::list(&**pool)
.await? .await?
.into_iter() .into_iter()
@@ -329,7 +352,8 @@ pub async fn license_delete(
check_is_admin_from_headers(req.headers(), &**pool).await?; check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = license.into_inner().0; let name = license.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = License::remove(&name, &mut transaction).await?; let result = License::remove(&name, &mut transaction).await?;
@@ -352,15 +376,18 @@ pub struct DonationPlatformQueryData {
} }
#[get("donation_platform")] #[get("donation_platform")]
pub async fn donation_platform_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> { pub async fn donation_platform_list(
let results: Vec<DonationPlatformQueryData> = DonationPlatform::list(&**pool) pool: web::Data<PgPool>,
.await? ) -> Result<HttpResponse, ApiError> {
.into_iter() let results: Vec<DonationPlatformQueryData> =
.map(|x| DonationPlatformQueryData { DonationPlatform::list(&**pool)
short: x.short, .await?
name: x.name, .into_iter()
}) .map(|x| DonationPlatformQueryData {
.collect(); short: x.short,
name: x.name,
})
.collect();
Ok(HttpResponse::Ok().json(results)) Ok(HttpResponse::Ok().json(results))
} }
@@ -398,7 +425,8 @@ pub async fn donation_platform_delete(
check_is_admin_from_headers(req.headers(), &**pool).await?; check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = loader.into_inner().0; let name = loader.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = DonationPlatform::remove(&name, &mut transaction).await?; let result = DonationPlatform::remove(&name, &mut transaction).await?;
@@ -415,7 +443,9 @@ pub async fn donation_platform_delete(
} }
#[get("report_type")] #[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?; let results = ReportType::list(&**pool).await?;
Ok(HttpResponse::Ok().json(results)) Ok(HttpResponse::Ok().json(results))
} }
@@ -444,7 +474,8 @@ pub async fn report_type_delete(
check_is_admin_from_headers(req.headers(), &**pool).await?; check_is_admin_from_headers(req.headers(), &**pool).await?;
let name = report_type.into_inner().0; let name = report_type.into_inner().0;
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; let mut transaction =
pool.begin().await.map_err(models::DatabaseError::from)?;
let result = ReportType::remove(&name, &mut transaction).await?; let result = ReportType::remove(&name, &mut transaction).await?;

View File

@@ -1,4 +1,6 @@
use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder}; use crate::database::models::notification_item::{
NotificationActionBuilder, NotificationBuilder,
};
use crate::database::models::TeamMember; use crate::database::models::TeamMember;
use crate::models::ids::ProjectId; use crate::models::ids::ProjectId;
use crate::models::teams::{Permissions, TeamId}; use crate::models::teams::{Permissions, TeamId};
@@ -17,23 +19,33 @@ pub async fn team_members_get_project(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0; let string = info.into_inner().0;
let project_data = 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 { 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 { if let Some(user) = current_user {
let team_member = let team_member = TeamMember::get_from_user_id(
TeamMember::get_from_user_id(project.team_id, user.id.into(), &**pool) project.team_id,
.await user.id.into(),
.map_err(ApiError::DatabaseError)?; &**pool,
)
.await
.map_err(ApiError::DatabaseError)?;
if team_member.is_some() { if team_member.is_some() {
let team_members: Vec<_> = members_data let team_members: Vec<_> = members_data
.into_iter() .into_iter()
.map(|data| crate::models::teams::TeamMember::from(data, false)) .map(|data| {
crate::models::teams::TeamMember::from(data, false)
})
.collect(); .collect();
return Ok(HttpResponse::Ok().json(team_members)); return Ok(HttpResponse::Ok().json(team_members));
@@ -59,14 +71,16 @@ pub async fn team_members_get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0; 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(); let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
if let Some(user) = current_user { if let Some(user) = current_user {
let team_member = TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool) let team_member =
.await TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
.map_err(ApiError::DatabaseError)?; .await
.map_err(ApiError::DatabaseError)?;
if team_member.is_some() { if team_member.is_some() {
let team_members: Vec<_> = members_data let team_members: Vec<_> = members_data
@@ -96,8 +110,12 @@ pub async fn join_team(
let team_id = info.into_inner().0.into(); let team_id = info.into_inner().0.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = let member = TeamMember::get_from_user_id_pending(
TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?; team_id,
current_user.id.into(),
&**pool,
)
.await?;
if let Some(member) = member { if let Some(member) = member {
if member.accepted { if member.accepted {
@@ -153,17 +171,20 @@ pub async fn add_team_member(
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let current_user = get_user_from_headers(req.headers(), &**pool).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) let member =
.await? TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
.ok_or_else(|| { .await?
ApiError::CustomAuthenticationError( .ok_or_else(|| {
"You don't have permission to edit members of this team".to_string(), ApiError::CustomAuthenticationError(
) "You don't have permission to edit members of this team"
})?; .to_string(),
)
})?;
if !member.permissions.contains(Permissions::MANAGE_INVITES) { if !member.permissions.contains(Permissions::MANAGE_INVITES) {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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) { if !member.permissions.contains(new_member.permissions) {
@@ -191,16 +212,23 @@ pub async fn add_team_member(
)); ));
} else { } else {
return Err(ApiError::InvalidInputError( return Err(ApiError::InvalidInputError(
"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) crate::database::models::User::get(member.user_id, &**pool)
.await? .await?
.ok_or_else(|| ApiError::InvalidInputError("An invalid User ID specified".to_string()))?; .ok_or_else(|| {
ApiError::InvalidInputError(
"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 { TeamMember {
id: new_id, id: new_id,
team_id, team_id,
@@ -232,11 +260,18 @@ pub async fn add_team_member(
"Team invite from {} to join the team for project {}", "Team invite from {} to join the team for project {}",
current_user.username, result.title current_user.username, result.title
), ),
link: format!("/{}/{}", result.project_type, ProjectId(result.id as u64)), link: format!(
"/{}/{}",
result.project_type,
ProjectId(result.id as u64)
),
actions: vec![ actions: vec![
NotificationActionBuilder { NotificationActionBuilder {
title: "Accept".to_string(), title: "Accept".to_string(),
action_route: ("POST".to_string(), format!("team/{}/join", team)), action_route: (
"POST".to_string(),
format!("team/{}/join", team),
),
}, },
NotificationActionBuilder { NotificationActionBuilder {
title: "Deny".to_string(), title: "Deny".to_string(),
@@ -273,20 +308,24 @@ pub async fn edit_team_member(
let user_id = ids.1.into(); let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) let member =
.await? TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.ok_or_else(|| { .await?
ApiError::CustomAuthenticationError( .ok_or_else(|| {
"You don't have permission to edit members of this team".to_string(), ApiError::CustomAuthenticationError(
) "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(|| { let edit_member_db =
ApiError::CustomAuthenticationError( TeamMember::get_from_user_id_pending(id, user_id, &**pool)
"You don't have permission to edit members of this team".to_string(), .await?
) .ok_or_else(|| {
})?; ApiError::CustomAuthenticationError(
"You don't have permission to edit members of this team"
.to_string(),
)
})?;
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
@@ -298,14 +337,16 @@ pub async fn edit_team_member(
if !member.permissions.contains(Permissions::EDIT_MEMBER) { if !member.permissions.contains(Permissions::EDIT_MEMBER) {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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 let Some(new_permissions) = edit_member.permissions {
if !member.permissions.contains(new_permissions) { if !member.permissions.contains(new_permissions) {
return Err(ApiError::InvalidInputError( return Err(ApiError::InvalidInputError(
"The new permissions have permissions that you don't have".to_string(), "The new permissions have permissions that you don't have"
.to_string(),
)); ));
} }
} }
@@ -346,22 +387,34 @@ pub async fn transfer_ownership(
let id = info.into_inner().0; let id = info.into_inner().0;
let current_user = get_user_from_headers(req.headers(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool) let member = TeamMember::get_from_user_id(
.await? id.into(),
.ok_or_else(|| { current_user.id.into(),
ApiError::CustomAuthenticationError( &**pool,
"You don't have permission to edit members of this team".to_string(), )
) .await?
})?; .ok_or_else(|| {
let new_member = TeamMember::get_from_user_id(id.into(), new_owner.user_id.into(), &**pool) ApiError::CustomAuthenticationError(
.await? "You don't have permission to edit members of this team"
.ok_or_else(|| { .to_string(),
ApiError::InvalidInputError("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::InvalidInputError(
"The new owner specified does not exist".to_string(),
)
})?;
if member.role != crate::models::teams::OWNER_ROLE { if member.role != crate::models::teams::OWNER_ROLE {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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(),
)); ));
} }
@@ -409,15 +462,18 @@ pub async fn remove_team_member(
let user_id = ids.1.into(); let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) let member =
.await? TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.ok_or_else(|| { .await?
ApiError::CustomAuthenticationError( .ok_or_else(|| {
"You don't have permission to edit members of this team".to_string(), ApiError::CustomAuthenticationError(
) "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 let Some(delete_member) = delete_member {
if delete_member.role == crate::models::teams::OWNER_ROLE { if delete_member.role == crate::models::teams::OWNER_ROLE {
@@ -431,7 +487,8 @@ pub async fn remove_team_member(
// Members other than the owner can either leave the team, or be // Members other than the owner can either leave the team, or be
// removed by a member with the REMOVE_MEMBER permission. // removed by a member with the REMOVE_MEMBER permission.
if delete_member.user_id == member.user_id 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, &**pool).await?; TeamMember::delete(id, user_id, &**pool).await?;
} else { } else {
@@ -440,7 +497,8 @@ pub async fn remove_team_member(
)); ));
} }
} else if delete_member.user_id == member.user_id } 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 // This is a pending invite rather than a member, so the
// user being invited or team members with the MANAGE_INVITES // user being invited or team members with the MANAGE_INVITES
@@ -448,7 +506,8 @@ pub async fn remove_team_member(
TeamMember::delete(id, user_id, &**pool).await?; TeamMember::delete(id, user_id, &**pool).await?;
} else { } else {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"You do not have permission to cancel a team invite".to_string(), "You do not have permission to cancel a team invite"
.to_string(),
)); ));
} }
Ok(HttpResponse::NoContent().body("")) Ok(HttpResponse::NoContent().body(""))

View File

@@ -20,9 +20,11 @@ pub async fn forge_updates(
let (id,) = info.into_inner(); let (id,) = info.into_inner();
let project = database::models::Project::get_full_from_slug_or_project_id(&id, &**pool) let project = database::models::Project::get_full_from_slug_or_project_id(
.await? &id, &**pool,
.ok_or_else(|| ApiError::InvalidInputError(ERROR.to_string()))?; )
.await?
.ok_or_else(|| ApiError::InvalidInputError(ERROR.to_string()))?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -38,7 +40,8 @@ pub async fn forge_updates(
) )
.await?; .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?;
versions.sort_by(|a, b| b.date_published.cmp(&a.date_published)); versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));
#[derive(Serialize)] #[derive(Serialize)]
@@ -48,7 +51,11 @@ pub async fn forge_updates(
} }
let mut response = ForgeUpdates { let mut response = ForgeUpdates {
homepage: format!("{}/mod/{}", dotenv::var("SITE_URL").unwrap_or_default(), id), homepage: format!(
"{}/mod/{}",
dotenv::var("SITE_URL").unwrap_or_default(),
id
),
promos: HashMap::new(), promos: HashMap::new(),
}; };

View File

@@ -20,8 +20,10 @@ pub async fn user_auth_get(
req: HttpRequest, req: HttpRequest,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok().json(
.json(get_user_from_headers(req.headers(), &mut *pool.acquire().await?).await?)) get_user_from_headers(req.headers(), &mut *pool.acquire().await?)
.await?,
))
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -41,7 +43,8 @@ pub async fn users_get(
let users_data = User::get_many(user_ids, &**pool).await?; 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)) Ok(HttpResponse::Ok().json(users))
} }
@@ -52,7 +55,8 @@ pub async fn user_get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0; 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; let mut user_data;
@@ -82,9 +86,11 @@ pub async fn projects_list(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await.ok(); let user = get_user_from_headers(req.headers(), &**pool).await.ok();
let id_option = let id_option = crate::database::models::User::get_id_from_username_or_id(
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) &*info.into_inner().0,
.await?; &**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
let user_id: UserId = id.into(); let user_id: UserId = id.into();
@@ -93,17 +99,24 @@ pub async fn projects_list(
if current_user.role.is_mod() || current_user.id == user_id { if current_user.role.is_mod() || current_user.id == user_id {
User::get_projects_private(id, &**pool).await? User::get_projects_private(id, &**pool).await?
} else { } else {
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? User::get_projects(
id,
ProjectStatus::Approved.as_str(),
&**pool,
)
.await?
} }
} else { } else {
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool)
.await?
}; };
let response: Vec<_> = crate::database::Project::get_many_full(project_data, &**pool) let response: Vec<_> =
.await? crate::database::Project::get_many_full(project_data, &**pool)
.into_iter() .await?
.map(Project::from) .into_iter()
.collect(); .map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(response)) Ok(HttpResponse::Ok().json(response))
} else { } else {
@@ -152,13 +165,15 @@ pub async fn user_edit(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
new_user new_user.validate().map_err(|err| {
.validate() ApiError::ValidationError(validation_errors_to_string(err, None))
.map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?; })?;
let id_option = let id_option = crate::database::models::User::get_id_from_username_or_id(
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) &*info.into_inner().0,
.await?; &**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
let user_id: UserId = id.into(); let user_id: UserId = id.into();
@@ -168,8 +183,10 @@ pub async fn user_edit(
if let Some(username) = &new_user.username { if let Some(username) = &new_user.username {
let user_option = let user_option =
crate::database::models::User::get_id_from_username_or_id(username, &**pool) crate::database::models::User::get_id_from_username_or_id(
.await?; username, &**pool,
)
.await?;
if user_option.is_none() { if user_option.is_none() {
sqlx::query!( sqlx::query!(
@@ -282,19 +299,23 @@ pub async fn user_icon_edit(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>, file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload, mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> { ) -> 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 = dotenv::var("CDN_URL")?; let cdn_url = dotenv::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = crate::database::models::User::get_id_from_username_or_id( let id_option =
&*info.into_inner().0, crate::database::models::User::get_id_from_username_or_id(
&**pool, &*info.into_inner().0,
) &**pool,
.await?; )
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if user.id != id.into() && !user.role.is_mod() { if user.id != id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"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(),
)); ));
} }
@@ -322,8 +343,12 @@ pub async fn user_icon_edit(
} }
} }
let bytes = let bytes = read_from_payload(
read_from_payload(&mut payload, 2097152, "Icons must be smaller than 2MiB").await?; &mut payload,
2097152,
"Icons must be smaller than 2MiB",
)
.await?;
let upload_data = file_host let upload_data = file_host
.upload_file( .upload_file(
@@ -374,9 +399,11 @@ pub async fn user_delete(
removal_type: web::Query<RemovalType>, removal_type: web::Query<RemovalType>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = let id_option = crate::database::models::User::get_id_from_username_or_id(
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) &*info.into_inner().0,
.await?; &**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_mod() && user.id != id.into() { if !user.role.is_mod() && user.id != id.into() {
@@ -389,9 +416,15 @@ pub async fn user_delete(
let result; let result;
if &*removal_type.removal_type == "full" { if &*removal_type.removal_type == "full" {
result = crate::database::models::User::remove_full(id, &mut transaction).await?; result = crate::database::models::User::remove_full(
id,
&mut transaction,
)
.await?;
} else { } else {
result = crate::database::models::User::remove(id, &mut transaction).await?; result =
crate::database::models::User::remove(id, &mut transaction)
.await?;
}; };
transaction.commit().await?; transaction.commit().await?;
@@ -413,9 +446,11 @@ pub async fn user_follows(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = let id_option = crate::database::models::User::get_id_from_username_or_id(
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) &*info.into_inner().0,
.await?; &**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_mod() && user.id != id.into() { if !user.role.is_mod() && user.id != id.into() {
@@ -441,11 +476,12 @@ pub async fn user_follows(
.try_collect::<Vec<crate::database::models::ProjectId>>() .try_collect::<Vec<crate::database::models::ProjectId>>()
.await?; .await?;
let projects: Vec<_> = crate::database::Project::get_many_full(project_ids, &**pool) let projects: Vec<_> =
.await? crate::database::Project::get_many_full(project_ids, &**pool)
.into_iter() .await?
.map(Project::from) .into_iter()
.collect(); .map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects)) Ok(HttpResponse::Ok().json(projects))
} else { } else {
@@ -460,9 +496,11 @@ pub async fn user_notifications(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = let id_option = crate::database::models::User::get_id_from_username_or_id(
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) &*info.into_inner().0,
.await?; &**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_mod() && user.id != id.into() { if !user.role.is_mod() && user.id != id.into() {

View File

@@ -30,15 +30,18 @@ pub async fn get_mods(
count.count as i64 count.count as i64
) )
.fetch_many(&**pool) .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>>() .try_collect::<Vec<database::models::ProjectId>>()
.await?; .await?;
let projects: Vec<_> = database::Project::get_many_full(project_ids, &**pool) let projects: Vec<_> =
.await? database::Project::get_many_full(project_ids, &**pool)
.into_iter() .await?
.map(Project::from) .into_iter()
.collect(); .map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects)) Ok(HttpResponse::Ok().json(projects))
} }

View File

@@ -1,6 +1,8 @@
use crate::file_hosting::FileHost; use crate::file_hosting::FileHost;
use crate::models::projects::SearchRequest; use crate::models::projects::SearchRequest;
use crate::routes::project_creation::{project_create_inner, undo_uploads, CreateError}; use crate::routes::project_creation::{
project_create_inner, undo_uploads, CreateError,
};
use crate::routes::projects::ProjectIds; use crate::routes::projects::ProjectIds;
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::search::{search_for_project, SearchConfig, SearchError}; use crate::search::{search_for_project, SearchConfig, SearchError};
@@ -89,12 +91,14 @@ pub async fn mods_get(
ids: web::Query<ProjectIds>, ids: web::Query<ProjectIds>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let project_ids = serde_json::from_str::<Vec<models::ids::ProjectId>>(&*ids.ids)? let project_ids =
.into_iter() serde_json::from_str::<Vec<models::ids::ProjectId>>(&*ids.ids)?
.map(|x| x.into()) .into_iter()
.collect(); .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 user_option = get_user_from_headers(req.headers(), &**pool).await.ok();

View File

@@ -2,7 +2,9 @@ use crate::models::ids::ReportId;
use crate::models::projects::{ProjectId, VersionId}; use crate::models::projects::{ProjectId, VersionId};
use crate::models::users::UserId; use crate::models::users::UserId;
use crate::routes::ApiError; 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::web; use actix_web::web;
use actix_web::{get, post, HttpRequest, HttpResponse}; use actix_web::{get, post, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@@ -56,24 +58,31 @@ pub async fn report_create(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let mut transaction = pool.begin().await?; 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(); let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await { while let Some(item) = body.next().await {
bytes.extend_from_slice(&item.map_err(|_| { bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError("Error while parsing request payload!".to_string()) ApiError::InvalidInputError(
"Error while parsing request payload!".to_string(),
)
})?); })?);
} }
let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?; 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( let report_type = crate::database::models::categories::ReportType::get_id(
&*new_report.report_type, &*new_report.report_type,
&mut *transaction, &mut *transaction,
) )
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
ApiError::InvalidInputError(format!("Invalid report type: {}", new_report.report_type)) ApiError::InvalidInputError(format!(
"Invalid report type: {}",
new_report.report_type
))
})?; })?;
let mut report = crate::database::models::report_item::Report { let mut report = crate::database::models::report_item::Report {
id, id,
@@ -89,17 +98,29 @@ pub async fn report_create(
match new_report.item_type { match new_report.item_type {
ItemType::Mod => { ItemType::Mod => {
report.project_id = Some( report.project_id = Some(
serde_json::from_str::<ProjectId>(&*format!("\"{}\"", new_report.item_id))?.into(), serde_json::from_str::<ProjectId>(&*format!(
"\"{}\"",
new_report.item_id
))?
.into(),
) )
} }
ItemType::Version => { ItemType::Version => {
report.version_id = Some( report.version_id = Some(
serde_json::from_str::<VersionId>(&*format!("\"{}\"", new_report.item_id))?.into(), serde_json::from_str::<VersionId>(&*format!(
"\"{}\"",
new_report.item_id
))?
.into(),
) )
} }
ItemType::User => { ItemType::User => {
report.user_id = Some( report.user_id = Some(
serde_json::from_str::<UserId>(&*format!("\"{}\"", new_report.item_id))?.into(), serde_json::from_str::<UserId>(&*format!(
"\"{}\"",
new_report.item_id
))?
.into(),
) )
} }
ItemType::Unknown => { ItemType::Unknown => {
@@ -160,8 +181,10 @@ pub async fn reports(
.try_collect::<Vec<crate::database::models::ids::ReportId>>() .try_collect::<Vec<crate::database::models::ids::ReportId>>()
.await?; .await?;
let query_reports = let query_reports = crate::database::models::report_item::Report::get_many(
crate::database::models::report_item::Report::get_many(report_ids, &**pool).await?; report_ids, &**pool,
)
.await?;
let mut reports = Vec::new(); let mut reports = Vec::new();

View File

@@ -1,4 +1,6 @@
use crate::database::models::categories::{Category, GameVersion, Loader, ProjectType}; use crate::database::models::categories::{
Category, GameVersion, Loader, ProjectType,
};
use crate::routes::ApiError; use crate::routes::ApiError;
use crate::util::auth::check_is_admin_from_headers; use crate::util::auth::check_is_admin_from_headers;
use actix_web::{get, put, web}; use actix_web::{get, put, web};
@@ -8,7 +10,9 @@ use sqlx::PgPool;
const DEFAULT_ICON: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>"#; const DEFAULT_ICON: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>"#;
#[get("category")] #[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) let results = Category::list(&**pool)
.await? .await?
.into_iter() .into_iter()
@@ -28,11 +32,16 @@ pub async fn category_create(
let name = category.into_inner().0; let name = category.into_inner().0;
let project_type = crate::database::models::ProjectTypeId::get_id("mod".to_string(), &**pool) let project_type = crate::database::models::ProjectTypeId::get_id(
.await? "mod".to_string(),
.ok_or_else(|| { &**pool,
ApiError::InvalidInputError("Specified project type does not exist!".to_string()) )
})?; .await?
.ok_or_else(|| {
ApiError::InvalidInputError(
"Specified project type does not exist!".to_string(),
)
})?;
let _id = Category::builder() let _id = Category::builder()
.name(&name)? .name(&name)?
@@ -45,7 +54,9 @@ pub async fn category_create(
} }
#[get("loader")] #[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 results = Loader::list(&**pool) let results = Loader::list(&**pool)
.await? .await?
.into_iter() .into_iter()
@@ -67,12 +78,16 @@ pub async fn loader_create(
let name = loader.into_inner().0; let name = loader.into_inner().0;
let mut transaction = pool.begin().await?; let mut transaction = pool.begin().await?;
let project_types = ProjectType::get_many_id(&["mod".to_string()], &mut *transaction).await?; let project_types =
ProjectType::get_many_id(&["mod".to_string()], &mut *transaction)
.await?;
let _id = Loader::builder() let _id = Loader::builder()
.name(&name)? .name(&name)?
.icon(DEFAULT_ICON)? .icon(DEFAULT_ICON)?
.supported_project_types(&*project_types.into_iter().map(|x| x.id).collect::<Vec<_>>())? .supported_project_types(
&*project_types.into_iter().map(|x| x.id).collect::<Vec<_>>(),
)?
.insert(&mut transaction) .insert(&mut transaction)
.await?; .await?;
@@ -92,11 +107,15 @@ pub async fn game_version_list(
query: web::Query<GameVersionQueryData>, query: web::Query<GameVersionQueryData>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
if query.type_.is_some() || query.major.is_some() { if query.type_.is_some() || query.major.is_some() {
let results = GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool) let results = GameVersion::list_filter(
.await? query.type_.as_deref(),
.into_iter() query.major,
.map(|x| x.version) &**pool,
.collect::<Vec<String>>(); )
.await?
.into_iter()
.map(|x| x.version)
.collect::<Vec<String>>();
Ok(HttpResponse::Ok().json(results)) Ok(HttpResponse::Ok().json(results))
} else { } else {
let results = GameVersion::list(&**pool) let results = GameVersion::list(&**pool)

View File

@@ -29,18 +29,20 @@ pub async fn team_members_get(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0; let id = info.into_inner().0;
let members_data = let members_data =
crate::database::models::TeamMember::get_from_team(id.into(), &**pool).await?; crate::database::models::TeamMember::get_from_team(id.into(), &**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 { if let Some(user) = current_user {
let team_member = crate::database::models::TeamMember::get_from_user_id( let team_member =
id.into(), crate::database::models::TeamMember::get_from_user_id(
user.id.into(), id.into(),
&**pool, user.id.into(),
) &**pool,
.await )
.map_err(ApiError::DatabaseError)?; .await
.map_err(ApiError::DatabaseError)?;
if team_member.is_some() { if team_member.is_some() {
let team_members: Vec<TeamMember> = members_data let team_members: Vec<TeamMember> = members_data

View File

@@ -15,9 +15,11 @@ pub async fn mods_list(
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await.ok(); let user = get_user_from_headers(req.headers(), &**pool).await.ok();
let id_option = let id_option = crate::database::models::User::get_id_from_username_or_id(
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) &*info.into_inner().0,
.await?; &**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
let user_id: UserId = id.into(); let user_id: UserId = id.into();
@@ -26,10 +28,16 @@ pub async fn mods_list(
if current_user.role.is_mod() || current_user.id == user_id { if current_user.role.is_mod() || current_user.id == user_id {
User::get_projects_private(id, &**pool).await? User::get_projects_private(id, &**pool).await?
} else { } else {
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? User::get_projects(
id,
ProjectStatus::Approved.as_str(),
&**pool,
)
.await?
} }
} else { } else {
User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool)
.await?
}; };
let response = project_data let response = project_data
@@ -50,9 +58,11 @@ pub async fn user_follows(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> { ) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?; let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = let id_option = crate::database::models::User::get_id_from_username_or_id(
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) &*info.into_inner().0,
.await?; &**pool,
)
.await?;
if let Some(id) = id_option { if let Some(id) = id_option {
if !user.role.is_mod() && user.id != id.into() { if !user.role.is_mod() && user.id != id.into() {
@@ -71,7 +81,9 @@ pub async fn user_follows(
id as crate::database::models::ids::UserId, id as crate::database::models::ids::UserId,
) )
.fetch_many(&**pool) .fetch_many(&**pool)
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.mod_id as u64))) }) .try_filter_map(|e| async {
Ok(e.right().map(|m| ProjectId(m.mod_id as u64)))
})
.try_collect::<Vec<ProjectId>>() .try_collect::<Vec<ProjectId>>()
.await?; .await?;

View File

@@ -1,9 +1,12 @@
use crate::database::models; use crate::database::models;
use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder}; use crate::database::models::version_item::{
VersionBuilder, VersionFileBuilder,
};
use crate::file_hosting::FileHost; use crate::file_hosting::FileHost;
use crate::models::projects::{ use crate::models::projects::{
Dependency, GameVersion, Loader, ProjectId, Version, VersionFile, VersionId, VersionType, Dependency, GameVersion, Loader, ProjectId, Version, VersionFile,
VersionId, VersionType,
}; };
use crate::models::teams::Permissions; use crate::models::teams::Permissions;
use crate::routes::project_creation::{CreateError, UploadedFile}; use crate::routes::project_creation::{CreateError, UploadedFile};
@@ -74,8 +77,11 @@ pub async fn version_create(
.await; .await;
if result.is_err() { if result.is_err() {
let undo_result = let undo_result = super::project_creation::undo_uploads(
super::project_creation::undo_uploads(&***file_host, &uploaded_files).await; &***file_host,
&uploaded_files,
)
.await;
let rollback_result = transaction.rollback().await; let rollback_result = transaction.rollback().await;
if let Err(e) = undo_result { if let Err(e) = undo_result {
@@ -103,25 +109,30 @@ async fn version_create_inner(
let mut initial_version_data = None; let mut initial_version_data = None;
let mut version_builder = None; let mut version_builder = None;
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; let all_game_versions =
let all_loaders = models::categories::Loader::list(&mut *transaction).await?; 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?; let user = get_user_from_headers(req.headers(), &mut *transaction).await?;
while let Some(item) = payload.next().await { while let Some(item) = payload.next().await {
let mut field: Field = item.map_err(CreateError::MultipartError)?; let mut field: Field = item.map_err(CreateError::MultipartError)?;
let content_disposition = field.content_disposition().clone(); let content_disposition = field.content_disposition().clone();
let name = content_disposition let name = content_disposition.get_name().ok_or_else(|| {
.get_name() CreateError::MissingValueError("Missing content name".to_string())
.ok_or_else(|| CreateError::MissingValueError("Missing content name".to_string()))?; })?;
if name == "data" { if name == "data" {
let mut data = Vec::new(); let mut data = Vec::new();
while let Some(chunk) = field.next().await { 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 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); 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() { if version_create_data.project_id.is_none() {
@@ -131,10 +142,13 @@ async fn version_create_inner(
} }
version_create_data.validate().map_err(|err| { version_create_data.validate().map_err(|err| {
CreateError::ValidationError(validation_errors_to_string(err, None)) CreateError::ValidationError(validation_errors_to_string(
err, None,
))
})?; })?;
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 // Ensure that the project this version is being added to exists
let results = sqlx::query!( let results = sqlx::query!(
@@ -162,7 +176,8 @@ async fn version_create_inner(
if results.exists.unwrap_or(true) { if results.exists.unwrap_or(true) {
return Err(CreateError::InvalidInput( return Err(CreateError::InvalidInput(
"A version with that version_number already exists".to_string(), "A version with that version_number already exists"
.to_string(),
)); ));
} }
@@ -176,7 +191,8 @@ async fn version_create_inner(
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
CreateError::CustomAuthenticationError( CreateError::CustomAuthenticationError(
"You don't have permission to upload this version!".to_string(), "You don't have permission to upload this version!"
.to_string(),
) )
})?; })?;
@@ -185,11 +201,13 @@ async fn version_create_inner(
.contains(Permissions::UPLOAD_VERSION) .contains(Permissions::UPLOAD_VERSION)
{ {
return Err(CreateError::CustomAuthenticationError( 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!( let project_type = sqlx::query!(
" "
@@ -210,7 +228,9 @@ async fn version_create_inner(
all_game_versions all_game_versions
.iter() .iter()
.find(|y| y.version == x.0) .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) .map(|y| y.id)
}) })
.collect::<Result<Vec<models::GameVersionId>, CreateError>>()?; .collect::<Result<Vec<models::GameVersionId>, CreateError>>()?;
@@ -222,7 +242,9 @@ async fn version_create_inner(
all_loaders all_loaders
.iter() .iter()
.find(|y| { .find(|y| {
y.loader == x.0 && y.supported_project_types.contains(&project_type) y.loader == x.0
&& y.supported_project_types
.contains(&project_type)
}) })
.ok_or_else(|| CreateError::InvalidLoader(x.0.clone())) .ok_or_else(|| CreateError::InvalidLoader(x.0.clone()))
.map(|y| y.id) .map(|y| y.id)
@@ -261,7 +283,9 @@ async fn version_create_inner(
} }
let version = version_builder.as_mut().ok_or_else(|| { 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!( let project_type = sqlx::query!(
@@ -276,9 +300,9 @@ async fn version_create_inner(
.await? .await?
.name; .name;
let version_data = initial_version_data let version_data = initial_version_data.clone().ok_or_else(|| {
.clone() CreateError::InvalidInput("`data` field is required".to_string())
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?; })?;
upload_file( upload_file(
&mut field, &mut field,
@@ -300,10 +324,12 @@ async fn version_create_inner(
.await?; .await?;
} }
let version_data = initial_version_data let version_data = initial_version_data.ok_or_else(|| {
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?; CreateError::InvalidInput("`data` field is required".to_string())
let builder = version_builder })?;
.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() { if builder.files.is_empty() {
return Err(CreateError::InvalidInput( return Err(CreateError::InvalidInput(
@@ -433,8 +459,11 @@ pub async fn upload_file_to_version(
.await; .await;
if result.is_err() { if result.is_err() {
let undo_result = let undo_result = super::project_creation::undo_uploads(
super::project_creation::undo_uploads(&***file_host, &uploaded_files).await; &***file_host,
&uploaded_files,
)
.await;
let rollback_result = transaction.rollback().await; let rollback_result = transaction.rollback().await;
if let Err(e) = undo_result { if let Err(e) = undo_result {
@@ -477,21 +506,26 @@ async fn upload_file_to_version_inner(
} }
}; };
let team_member = let team_member = models::TeamMember::get_from_user_id_version(
models::TeamMember::get_from_user_id_version(version_id, user.id.into(), &mut *transaction) version_id,
.await? user.id.into(),
.ok_or_else(|| { &mut *transaction,
CreateError::CustomAuthenticationError( )
"You don't have permission to upload files to this version!".to_string(), .await?
) .ok_or_else(|| {
})?; CreateError::CustomAuthenticationError(
"You don't have permission to upload files to this version!"
.to_string(),
)
})?;
if !team_member if !team_member
.permissions .permissions
.contains(Permissions::UPLOAD_VERSION) .contains(Permissions::UPLOAD_VERSION)
{ {
return Err(CreateError::CustomAuthenticationError( 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(),
)); ));
} }
@@ -510,19 +544,22 @@ async fn upload_file_to_version_inner(
.await? .await?
.name; .name;
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; let all_game_versions =
models::categories::GameVersion::list(&mut *transaction).await?;
while let Some(item) = payload.next().await { while let Some(item) = payload.next().await {
let mut field: Field = item.map_err(CreateError::MultipartError)?; let mut field: Field = item.map_err(CreateError::MultipartError)?;
let content_disposition = field.content_disposition().clone(); let content_disposition = field.content_disposition().clone();
let name = content_disposition let name = content_disposition.get_name().ok_or_else(|| {
.get_name() CreateError::MissingValueError("Missing content name".to_string())
.ok_or_else(|| CreateError::MissingValueError("Missing content name".to_string()))?; })?;
if name == "data" { if name == "data" {
let mut data = Vec::new(); let mut data = Vec::new();
while let Some(chunk) = field.next().await { 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 file_data: InitialFileData = serde_json::from_slice(&data)?; let file_data: InitialFileData = serde_json::from_slice(&data)?;
// TODO: currently no data here, but still required // TODO: currently no data here, but still required
@@ -532,7 +569,9 @@ async fn upload_file_to_version_inner(
} }
let _file_data = initial_file_data.as_ref().ok_or_else(|| { 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",
))
})?; })?;
upload_file( upload_file(
@@ -596,7 +635,9 @@ pub async fn upload_file(
let (file_name, file_extension) = get_name_ext(content_disposition)?; let (file_name, file_extension) = get_name_ext(content_disposition)?;
let content_type = crate::util::ext::project_file_type(file_extension) 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( let data = read_from_field(
field, 100 * (1 << 20), field, 100 * (1 << 20),
@@ -619,7 +660,8 @@ pub async fn upload_file(
if exists { if exists {
return Err(CreateError::InvalidInput( 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(),
)); ));
} }
@@ -683,9 +725,9 @@ pub async fn upload_file(
pub fn get_name_ext( pub fn get_name_ext(
content_disposition: &actix_web::http::header::ContentDisposition, content_disposition: &actix_web::http::header::ContentDisposition,
) -> Result<(&str, &str), CreateError> { ) -> Result<(&str, &str), CreateError> {
let file_name = content_disposition let file_name = content_disposition.get_filename().ok_or_else(|| {
.get_filename() CreateError::MissingValueError("Missing content file name".to_string())
.ok_or_else(|| CreateError::MissingValueError("Missing content file name".to_string()))?; })?;
let file_extension = if let Some(last_period) = file_name.rfind('.') { let file_extension = if let Some(last_period) = file_name.rfind('.') {
file_name.get((last_period + 1)..).unwrap_or("") file_name.get((last_period + 1)..).unwrap_or("")
} else { } else {

View File

@@ -128,25 +128,28 @@ pub async fn delete_file(
if let Some(row) = result { if let Some(row) = result {
if !user.role.is_mod() { if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id_version( let team_member =
database::models::ids::VersionId(row.version_id), database::models::TeamMember::get_from_user_id_version(
user.id.into(), database::models::ids::VersionId(row.version_id),
&**pool, user.id.into(),
) &**pool,
.await
.map_err(ApiError::DatabaseError)?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
"You don't have permission to delete this file!".to_string(),
) )
})?; .await
.map_err(ApiError::DatabaseError)?
.ok_or_else(|| {
ApiError::CustomAuthenticationError(
"You don't have permission to delete this file!"
.to_string(),
)
})?;
if !team_member if !team_member
.permissions .permissions
.contains(Permissions::DELETE_VERSION) .contains(Permissions::DELETE_VERSION)
{ {
return Err(ApiError::CustomAuthenticationError( return Err(ApiError::CustomAuthenticationError(
"You don't have permission to delete this file!".to_string(), "You don't have permission to delete this file!"
.to_string(),
)); ));
} }
} }
@@ -167,7 +170,8 @@ pub async fn delete_file(
if files.len() < 2 { if files.len() < 2 {
return Err(ApiError::InvalidInputError( return Err(ApiError::InvalidInputError(
"Versions must have at least one file uploaded to them".to_string(), "Versions must have at least one file uploaded to them"
.to_string(),
)); ));
} }
@@ -269,7 +273,9 @@ pub async fn get_update_from_hash(
.await?; .await?;
if let Some(version_id) = version_ids.last() { if let Some(version_id) = version_ids.last() {
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) ok_or_not_found::<QueryVersion, Version>(version_data)
} else { } else {
@@ -436,12 +442,15 @@ pub async fn update_files(
} }
} }
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 response = HashMap::new(); let mut response = HashMap::new();
for row in &result { for row in &result {
if let Some(version) = versions.iter().find(|x| x.id.0 == row.version_id) { if let Some(version) =
versions.iter().find(|x| x.id.0 == row.version_id)
{
response.insert( response.insert(
hex::encode(&row.hash), hex::encode(&row.hash),
models::projects::Version::from(version.clone()), models::projects::Version::from(version.clone()),

View File

@@ -18,8 +18,8 @@ impl Scheduler {
F: FnMut() -> R + Send + 'static, F: FnMut() -> R + Send + 'static,
R: std::future::Future<Output = ()> + Send + 'static, R: std::future::Future<Output = ()> + Send + 'static,
{ {
let future = let future = IntervalStream::new(time::interval(interval))
IntervalStream::new(time::interval(interval)).for_each_concurrent(2, move |_| task()); .for_each_concurrent(2, move |_| task());
self.arbiter.spawn(future); self.arbiter.spawn(future);
} }
@@ -38,8 +38,9 @@ pub fn schedule_versions(
pool: sqlx::Pool<sqlx::Postgres>, pool: sqlx::Pool<sqlx::Postgres>,
skip_initial: bool, skip_initial: bool,
) { ) {
let version_index_interval = let version_index_interval = std::time::Duration::from_secs(
std::time::Duration::from_secs(parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800)); parse_var("VERSION_INDEX_INTERVAL").unwrap_or(1800),
);
let mut skip = skip_initial; let mut skip = skip_initial;
scheduler.run(version_index_interval, move || { scheduler.run(version_index_interval, move || {
@@ -90,11 +91,15 @@ struct VersionFormat<'a> {
release_time: chrono::DateTime<chrono::Utc>, release_time: chrono::DateTime<chrono::Utc>,
} }
async fn update_versions(pool: &sqlx::Pool<sqlx::Postgres>) -> Result<(), VersionIndexingError> { async fn update_versions(
let input = reqwest::get("https://launchermeta.mojang.com/mc/game/version_manifest.json") pool: &sqlx::Pool<sqlx::Postgres>,
.await? ) -> Result<(), VersionIndexingError> {
.json::<InputFormat>() let input = reqwest::get(
.await?; "https://launchermeta.mojang.com/mc/game/version_manifest.json",
)
.await?
.json::<InputFormat>()
.await?;
let mut skipped_versions_count = 0u32; let mut skipped_versions_count = 0u32;
@@ -156,7 +161,8 @@ async fn update_versions(pool: &sqlx::Pool<sqlx::Postgres>) -> Result<(), Versio
.chars() .chars()
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
{ {
if let Some((_, alternate)) = HALL_OF_SHAME.iter().find(|(version, _)| name == *version) if let Some((_, alternate)) =
HALL_OF_SHAME.iter().find(|(version, _)| name == *version)
{ {
name = String::from(*alternate); name = String::from(*alternate);
} else { } else {

View File

@@ -7,7 +7,9 @@ use crate::search::UploadSearchProject;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
// TODO: Move this away from STRING_AGG to multiple queries - however this may be more efficient? // TODO: Move this away from STRING_AGG to multiple queries - however this may be more efficient?
pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, IndexingError> { pub async fn index_local(
pool: PgPool,
) -> Result<Vec<UploadSearchProject>, IndexingError> {
info!("Indexing local projects!"); info!("Indexing local projects!");
Ok( Ok(
sqlx::query!( sqlx::query!(

View File

@@ -88,15 +88,25 @@ async fn update_index_helper<'a>(
.await .await
} }
pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingError> { pub async fn reconfigure_indices(
config: &SearchConfig,
) -> Result<(), IndexingError> {
let client = config.make_client(); let client = config.make_client();
// Relevance Index // Relevance Index
update_index_helper(&client, "relevance_projects", "desc(downloads)").await?; update_index_helper(&client, "relevance_projects", "desc(downloads)")
update_index_helper(&client, "downloads_projects", "desc(downloads)").await?; .await?;
update_index_helper(&client, "downloads_projects", "desc(downloads)")
.await?;
update_index_helper(&client, "follows_projects", "desc(follows)").await?; update_index_helper(&client, "follows_projects", "desc(follows)").await?;
update_index_helper(&client, "updated_projects", "desc(modified_timestamp)").await?; update_index_helper(
update_index_helper(&client, "newest_projects", "desc(created_timestamp)").await?; &client,
"updated_projects",
"desc(modified_timestamp)",
)
.await?;
update_index_helper(&client, "newest_projects", "desc(created_timestamp)")
.await?;
Ok(()) Ok(())
} }
@@ -150,7 +160,10 @@ async fn create_index<'a>(
} }
} }
async fn add_to_index(index: Index<'_>, mods: &[UploadSearchProject]) -> Result<(), IndexingError> { async fn add_to_index(
index: Index<'_>,
mods: &[UploadSearchProject],
) -> Result<(), IndexingError> {
for chunk in mods.chunks(MEILISEARCH_CHUNK_SIZE) { for chunk in mods.chunks(MEILISEARCH_CHUNK_SIZE) {
index.add_documents(chunk, Some("project_id")).await?; index.add_documents(chunk, Some("project_id")).await?;
} }
@@ -179,9 +192,27 @@ pub async fn add_projects(
) -> Result<(), IndexingError> { ) -> Result<(), IndexingError> {
let client = config.make_client(); let client = config.make_client();
create_and_add_to_index(&client, &projects, "relevance_projects", "desc(downloads)").await?; create_and_add_to_index(
create_and_add_to_index(&client, &projects, "downloads_projects", "desc(downloads)").await?; &client,
create_and_add_to_index(&client, &projects, "follows_projects", "desc(follows)").await?; &projects,
"relevance_projects",
"desc(downloads)",
)
.await?;
create_and_add_to_index(
&client,
&projects,
"downloads_projects",
"desc(downloads)",
)
.await?;
create_and_add_to_index(
&client,
&projects,
"follows_projects",
"desc(follows)",
)
.await?;
create_and_add_to_index( create_and_add_to_index(
&client, &client,
&projects, &projects,

View File

@@ -21,9 +21,15 @@ impl CreationQueue {
self.queue.lock().unwrap().push(search_project); self.queue.lock().unwrap().push(search_project);
} }
pub fn take(&self) -> Vec<UploadSearchProject> { pub fn take(&self) -> Vec<UploadSearchProject> {
std::mem::replace(&mut *self.queue.lock().unwrap(), Vec::with_capacity(10)) std::mem::replace(
&mut *self.queue.lock().unwrap(),
Vec::with_capacity(10),
)
} }
pub async fn index(&self, config: &SearchConfig) -> Result<(), IndexingError> { pub async fn index(
&self,
config: &SearchConfig,
) -> Result<(), IndexingError> {
let queue = self.take(); let queue = self.take();
add_projects(queue, config).await add_projects(queue, config).await
} }

View File

@@ -149,12 +149,13 @@ pub async fn search_for_project(
) -> Result<SearchResults, SearchError> { ) -> Result<SearchResults, SearchError> {
let client = Client::new(&*config.address, &*config.key); let client = Client::new(&*config.address, &*config.key);
let filters: Cow<_> = match (info.filters.as_deref(), info.version.as_deref()) { let filters: Cow<_> =
(Some(f), Some(v)) => format!("({}) AND ({})", f, v).into(), match (info.filters.as_deref(), info.version.as_deref()) {
(Some(f), None) => f.into(), (Some(f), Some(v)) => format!("({}) AND ({})", f, v).into(),
(None, Some(v)) => v.into(), (Some(f), None) => f.into(),
(None, None) => "".into(), (None, Some(v)) => v.into(),
}; (None, None) => "".into(),
};
let offset = info.offset.as_deref().unwrap_or("0").parse()?; let offset = info.offset.as_deref().unwrap_or("0").parse()?;
let index = info.index.as_deref().unwrap_or("relevance"); let index = info.index.as_deref().unwrap_or("relevance");

View File

@@ -58,7 +58,8 @@ where
{ {
let github_user = get_github_user_from_token(access_token).await?; let github_user = get_github_user_from_token(access_token).await?;
let res = models::User::get_from_github_id(github_user.id, executor).await?; let res =
models::User::get_from_github_id(github_user.id, executor).await?;
match res { match res {
Some(result) => Ok(User { Some(result) => Ok(User {

View File

@@ -18,7 +18,9 @@ pub async fn read_from_payload(
return Err(ApiError::InvalidInputError(String::from(err_msg))); return Err(ApiError::InvalidInputError(String::from(err_msg)));
} else { } else {
bytes.extend_from_slice(&item.map_err(|_| { bytes.extend_from_slice(&item.map_err(|_| {
ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string()) ApiError::InvalidInputError(
"Unable to parse bytes in payload sent!".to_string(),
)
})?); })?);
} }
} }
@@ -35,13 +37,17 @@ pub async fn read_from_field(
if bytes.len() >= cap { if bytes.len() >= cap {
return Err(CreateError::InvalidInput(String::from(err_msg))); return Err(CreateError::InvalidInput(String::from(err_msg)));
} else { } else {
bytes.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?); bytes.extend_from_slice(
&chunk.map_err(CreateError::MultipartError)?,
);
} }
} }
Ok(bytes) Ok(bytes)
} }
pub(crate) fn ok_or_not_found<T, U>(version_data: Option<T>) -> Result<HttpResponse, ApiError> pub(crate) fn ok_or_not_found<T, U>(
version_data: Option<T>,
) -> Result<HttpResponse, ApiError>
where where
U: From<T> + Serialize, U: From<T> + Serialize,
{ {

View File

@@ -4,11 +4,15 @@ use regex::Regex;
use validator::{ValidationErrors, ValidationErrorsKind}; use validator::{ValidationErrors, ValidationErrorsKind};
lazy_static! { lazy_static! {
pub static ref RE_URL_SAFE: Regex = Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap(); pub static ref RE_URL_SAFE: Regex =
Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
} }
//TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future! //TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future!
pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option<String>) -> String { pub fn validation_errors_to_string(
errors: ValidationErrors,
adder: Option<String>,
) -> String {
let mut output = String::new(); let mut output = String::new();
let map = errors.into_errors(); let map = errors.into_errors();
@@ -19,13 +23,19 @@ pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option<Strin
if let Some(error) = map.get(field) { if let Some(error) = map.get(field) {
return match error { return match error {
ValidationErrorsKind::Struct(errors) => { ValidationErrorsKind::Struct(errors) => {
validation_errors_to_string(*errors.clone(), Some(format!("of item {}", field))) validation_errors_to_string(
*errors.clone(),
Some(format!("of item {}", field)),
)
} }
ValidationErrorsKind::List(list) => { ValidationErrorsKind::List(list) => {
if let Some((index, errors)) = list.iter().next() { if let Some((index, errors)) = list.iter().next() {
output.push_str(&*validation_errors_to_string( output.push_str(&*validation_errors_to_string(
*errors.clone(), *errors.clone(),
Some(format!("of list {} with index {}", index, field)), Some(format!(
"of list {} with index {}",
index, field
)),
)); ));
} }

View File

@@ -1,4 +1,6 @@
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; use crate::validate::{
SupportedGameVersions, ValidationError, ValidationResult,
};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;
@@ -31,13 +33,14 @@ impl super::Validator for FabricValidator {
archive: &mut ZipArchive<Cursor<bytes::Bytes>>, archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
) -> Result<ValidationResult, ValidationError> { ) -> Result<ValidationResult, ValidationError> {
archive.by_name("fabric.mod.json").map_err(|_| { archive.by_name("fabric.mod.json").map_err(|_| {
ValidationError::InvalidInputError("No fabric.mod.json present for Fabric file.".into()) ValidationError::InvalidInputError(
"No fabric.mod.json present for Fabric file.".into(),
)
})?; })?;
if !archive if !archive.file_names().any(|name| {
.file_names() name.ends_with("refmap.json") || name.ends_with(".class")
.any(|name| name.ends_with("refmap.json") || name.ends_with(".class")) }) {
{
return Ok(ValidationResult::Warning( return Ok(ValidationResult::Warning(
"Fabric mod file is a source file!", "Fabric mod file is a source file!",
)); ));

View File

@@ -1,4 +1,6 @@
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; use crate::validate::{
SupportedGameVersions, ValidationError, ValidationResult,
};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
use std::io::Cursor; use std::io::Cursor;
use zip::ZipArchive; use zip::ZipArchive;
@@ -31,7 +33,9 @@ impl super::Validator for ForgeValidator {
archive: &mut ZipArchive<Cursor<bytes::Bytes>>, archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
) -> Result<ValidationResult, ValidationError> { ) -> Result<ValidationResult, ValidationError> {
archive.by_name("META-INF/mods.toml").map_err(|_| { archive.by_name("META-INF/mods.toml").map_err(|_| {
ValidationError::InvalidInputError("No mods.toml present for Forge file.".into()) ValidationError::InvalidInputError(
"No mods.toml present for Forge file.".into(),
)
})?; })?;
if !archive.file_names().any(|name| name.ends_with(".class")) { if !archive.file_names().any(|name| name.ends_with(".class")) {
@@ -64,8 +68,14 @@ impl super::Validator for LegacyForgeValidator {
fn get_supported_game_versions(&self) -> SupportedGameVersions { fn get_supported_game_versions(&self) -> SupportedGameVersions {
// Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods // Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods
SupportedGameVersions::Range( SupportedGameVersions::Range(
DateTime::from_utc(NaiveDateTime::from_timestamp(1366818300, 0), Utc), DateTime::from_utc(
DateTime::from_utc(NaiveDateTime::from_timestamp(1505810340, 0), Utc), NaiveDateTime::from_timestamp(1366818300, 0),
Utc,
),
DateTime::from_utc(
NaiveDateTime::from_timestamp(1505810340, 0),
Utc,
),
) )
} }
@@ -74,7 +84,9 @@ impl super::Validator for LegacyForgeValidator {
archive: &mut ZipArchive<Cursor<bytes::Bytes>>, archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
) -> Result<ValidationResult, ValidationError> { ) -> Result<ValidationResult, ValidationError> {
archive.by_name("mcmod.info").map_err(|_| { archive.by_name("mcmod.info").map_err(|_| {
ValidationError::InvalidInputError("No mcmod.info present for Forge file.".into()) ValidationError::InvalidInputError(
"No mcmod.info present for Forge file.".into(),
)
})?; })?;
if !archive.file_names().any(|name| name.ends_with(".class")) { if !archive.file_names().any(|name| name.ends_with(".class")) {

View File

@@ -114,20 +114,24 @@ fn game_version_supported(
) -> bool { ) -> bool {
match supported_game_versions { match supported_game_versions {
SupportedGameVersions::All => true, SupportedGameVersions::All => true,
SupportedGameVersions::PastDate(date) => game_versions.iter().any(|x| { SupportedGameVersions::PastDate(date) => {
all_game_versions game_versions.iter().any(|x| {
.iter() all_game_versions
.find(|y| y.version == x.0) .iter()
.map(|x| x.date > date) .find(|y| y.version == x.0)
.unwrap_or(false) .map(|x| x.date > date)
}), .unwrap_or(false)
SupportedGameVersions::Range(before, after) => game_versions.iter().any(|x| { })
all_game_versions }
.iter() SupportedGameVersions::Range(before, after) => {
.find(|y| y.version == x.0) game_versions.iter().any(|x| {
.map(|x| x.date > before && x.date < after) all_game_versions
.unwrap_or(false) .iter()
}), .find(|y| y.version == x.0)
.map(|x| x.date > before && x.date < after)
.unwrap_or(false)
})
}
SupportedGameVersions::Custom(versions) => { SupportedGameVersions::Custom(versions) => {
versions.iter().any(|x| game_versions.contains(x)) versions.iter().any(|x| game_versions.contains(x))
} }

View File

@@ -1,7 +1,9 @@
use crate::models::projects::SideType; use crate::models::projects::SideType;
use crate::util::env::parse_strings_from_var; use crate::util::env::parse_strings_from_var;
use crate::util::validate::validation_errors_to_string; use crate::util::validate::validation_errors_to_string;
use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; use crate::validate::{
SupportedGameVersions, ValidationError, ValidationResult,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::{Cursor, Read}; use std::io::{Cursor, Read};
use std::path::Component; use std::path::Component;
@@ -33,7 +35,9 @@ pub struct PackFile<'a> {
pub downloads: Vec<&'a str>, pub downloads: Vec<&'a str>,
} }
fn validate_download_url(values: &[&str]) -> Result<(), validator::ValidationError> { fn validate_download_url(
values: &[&str],
) -> Result<(), validator::ValidationError> {
for value in values { for value in values {
let url = url::Url::parse(value) let url = url::Url::parse(value)
.ok() .ok()
@@ -43,7 +47,8 @@ fn validate_download_url(values: &[&str]) -> Result<(), validator::ValidationErr
return Err(validator::ValidationError::new("invalid URL")); return Err(validator::ValidationError::new("invalid URL"));
} }
let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS").unwrap_or_default(); let domains = parse_strings_from_var("WHITELISTED_MODPACK_DOMAINS")
.unwrap_or_default();
if !domains.contains( if !domains.contains(
&url.domain() &url.domain()
.ok_or_else(|| validator::ValidationError::new("invalid URL"))? .ok_or_else(|| validator::ValidationError::new("invalid URL"))?
@@ -131,9 +136,12 @@ impl super::Validator for PackValidator {
&self, &self,
archive: &mut ZipArchive<Cursor<bytes::Bytes>>, archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
) -> Result<ValidationResult, ValidationError> { ) -> Result<ValidationResult, ValidationError> {
let mut file = archive let mut file =
.by_name("modrinth.index.json") archive.by_name("modrinth.index.json").map_err(|_| {
.map_err(|_| ValidationError::InvalidInputError("Pack manifest is missing.".into()))?; ValidationError::InvalidInputError(
"Pack manifest is missing.".into(),
)
})?;
let mut contents = String::new(); let mut contents = String::new();
file.read_to_string(&mut contents)?; file.read_to_string(&mut contents)?;
@@ -141,7 +149,9 @@ impl super::Validator for PackValidator {
let pack: PackFormat = serde_json::from_str(&contents)?; let pack: PackFormat = serde_json::from_str(&contents)?;
pack.validate().map_err(|err| { pack.validate().map_err(|err| {
ValidationError::InvalidInputError(validation_errors_to_string(err, None).into()) ValidationError::InvalidInputError(
validation_errors_to_string(err, None).into(),
)
})?; })?;
if pack.game != "minecraft" { if pack.game != "minecraft" {
@@ -161,7 +171,9 @@ impl super::Validator for PackValidator {
.components() .components()
.next() .next()
.ok_or_else(|| { .ok_or_else(|| {
ValidationError::InvalidInputError("Invalid pack file path!".into()) ValidationError::InvalidInputError(
"Invalid pack file path!".into(),
)
})?; })?;
match path { match path {