You've already forked AstralRinth
forked from didirus/AstralRinth
Allow gallery featuring, add gallery images to search, rename rejection reasons, transfer ownership route (#226)
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE mods
|
||||||
|
RENAME COLUMN rejection_reason TO moderation_message;
|
||||||
|
ALTER TABLE mods
|
||||||
|
RENAME COLUMN rejection_body TO moderation_message_body;
|
||||||
|
|
||||||
|
ALTER TABLE mods_gallery
|
||||||
|
ADD COLUMN featured boolean default false;
|
||||||
1876
sqlx-data.json
1876
sqlx-data.json
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ impl DonationUrl {
|
|||||||
pub struct GalleryItem {
|
pub struct GalleryItem {
|
||||||
pub project_id: ProjectId,
|
pub project_id: ProjectId,
|
||||||
pub image_url: String,
|
pub image_url: String,
|
||||||
|
pub featured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GalleryItem {
|
impl GalleryItem {
|
||||||
@@ -48,14 +49,15 @@ impl GalleryItem {
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO mods_gallery (
|
INSERT INTO mods_gallery (
|
||||||
mod_id, image_url
|
mod_id, image_url, featured
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2
|
$1, $2, $3
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.project_id as ProjectId,
|
self.project_id as ProjectId,
|
||||||
self.image_url,
|
self.image_url,
|
||||||
|
self.featured
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -116,8 +118,8 @@ impl ProjectBuilder {
|
|||||||
server_side: self.server_side,
|
server_side: self.server_side,
|
||||||
license: self.license,
|
license: self.license,
|
||||||
slug: self.slug,
|
slug: self.slug,
|
||||||
rejection_reason: None,
|
moderation_message: None,
|
||||||
rejection_body: None,
|
moderation_message_body: None,
|
||||||
};
|
};
|
||||||
project_struct.insert(&mut *transaction).await?;
|
project_struct.insert(&mut *transaction).await?;
|
||||||
|
|
||||||
@@ -176,8 +178,8 @@ pub struct Project {
|
|||||||
pub server_side: SideTypeId,
|
pub server_side: SideTypeId,
|
||||||
pub license: LicenseId,
|
pub license: LicenseId,
|
||||||
pub slug: Option<String>,
|
pub slug: Option<String>,
|
||||||
pub rejection_reason: Option<String>,
|
pub moderation_message: Option<String>,
|
||||||
pub rejection_body: Option<String>,
|
pub moderation_message_body: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
@@ -242,7 +244,7 @@ impl Project {
|
|||||||
updated, status,
|
updated, status,
|
||||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||||
team_id, client_side, server_side, license, slug,
|
team_id, client_side, server_side, license, slug,
|
||||||
rejection_reason, rejection_body
|
moderation_message, moderation_message_body
|
||||||
FROM mods
|
FROM mods
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
",
|
",
|
||||||
@@ -275,8 +277,8 @@ impl Project {
|
|||||||
slug: row.slug,
|
slug: row.slug,
|
||||||
body: row.body,
|
body: row.body,
|
||||||
follows: row.follows,
|
follows: row.follows,
|
||||||
rejection_reason: row.rejection_reason,
|
moderation_message: row.moderation_message,
|
||||||
rejection_body: row.rejection_body,
|
moderation_message_body: row.moderation_message_body,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -300,7 +302,7 @@ impl Project {
|
|||||||
updated, status,
|
updated, status,
|
||||||
issues_url, source_url, wiki_url, discord_url, license_url,
|
issues_url, source_url, wiki_url, discord_url, license_url,
|
||||||
team_id, client_side, server_side, license, slug,
|
team_id, client_side, server_side, license, slug,
|
||||||
rejection_reason, rejection_body
|
moderation_message, moderation_message_body
|
||||||
FROM mods
|
FROM mods
|
||||||
WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))
|
WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||||
",
|
",
|
||||||
@@ -331,8 +333,8 @@ impl Project {
|
|||||||
slug: m.slug,
|
slug: m.slug,
|
||||||
body: m.body,
|
body: m.body,
|
||||||
follows: m.follows,
|
follows: m.follows,
|
||||||
rejection_reason: m.rejection_reason,
|
moderation_message: m.moderation_message,
|
||||||
rejection_body: m.rejection_body,
|
moderation_message_body: m.moderation_message_body,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<Project>>()
|
.try_collect::<Vec<Project>>()
|
||||||
@@ -589,9 +591,9 @@ impl Project {
|
|||||||
m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,
|
m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,
|
||||||
m.updated updated, m.status status,
|
m.updated updated, m.status status,
|
||||||
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
|
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
|
||||||
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body,
|
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
|
||||||
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
|
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
|
||||||
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions, STRING_AGG(DISTINCT mg.image_url, ',') gallery,
|
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions, STRING_AGG(DISTINCT mg.image_url || ', ' || mg.featured, ' ,') gallery,
|
||||||
STRING_AGG(DISTINCT md.joining_platform_id || ', ' || md.url || ', ' || dp.short || ', ' || dp.name, ' ,') donations
|
STRING_AGG(DISTINCT md.joining_platform_id || ', ' || md.url || ', ' || dp.short || ', ' || dp.name, ' ,') donations
|
||||||
FROM mods m
|
FROM mods m
|
||||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
||||||
@@ -638,22 +640,22 @@ impl Project {
|
|||||||
slug: m.slug.clone(),
|
slug: m.slug.clone(),
|
||||||
body: m.body.clone(),
|
body: m.body.clone(),
|
||||||
follows: m.follows,
|
follows: m.follows,
|
||||||
rejection_reason: m.rejection_reason,
|
moderation_message: m.moderation_message,
|
||||||
rejection_body: m.rejection_body,
|
moderation_message_body: m.moderation_message_body,
|
||||||
},
|
},
|
||||||
project_type: m.project_type_name,
|
project_type: m.project_type_name,
|
||||||
categories: m
|
categories: m
|
||||||
.categories
|
.categories
|
||||||
.unwrap_or_default()
|
.map(|x| x.split(',').map(|x| x.to_string()).collect())
|
||||||
.split(',')
|
.unwrap_or_default(),
|
||||||
.map(|x| x.to_string())
|
|
||||||
.collect(),
|
|
||||||
versions: m
|
versions: m
|
||||||
.versions
|
.versions
|
||||||
.unwrap_or_default()
|
.map(|x| {
|
||||||
.split(',')
|
x.split(',')
|
||||||
.map(|x| VersionId(x.parse().unwrap_or_default()))
|
.map(|x| VersionId(x.parse().unwrap_or_default()))
|
||||||
.collect(),
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
donation_urls: m
|
donation_urls: m
|
||||||
.donations
|
.donations
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -677,11 +679,22 @@ impl Project {
|
|||||||
.collect(),
|
.collect(),
|
||||||
gallery_items: m
|
gallery_items: m
|
||||||
.gallery
|
.gallery
|
||||||
.into_iter()
|
.unwrap_or_default()
|
||||||
.map(|x| GalleryItem {
|
.split(" ,")
|
||||||
project_id: id,
|
.map(|d| {
|
||||||
image_url: x,
|
let strings: Vec<&str> = d.split(", ").collect();
|
||||||
|
|
||||||
|
if strings.len() >= 2 {
|
||||||
|
Some(GalleryItem {
|
||||||
|
project_id: id,
|
||||||
|
image_url: strings[0].to_string(),
|
||||||
|
featured: strings[1].parse().unwrap_or(false),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.flatten()
|
||||||
.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,
|
||||||
@@ -710,9 +723,9 @@ impl Project {
|
|||||||
m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,
|
m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,
|
||||||
m.updated updated, m.status status,
|
m.updated updated, m.status status,
|
||||||
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
|
m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,
|
||||||
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.rejection_reason rejection_reason, m.rejection_body rejection_body,
|
m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
|
||||||
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
|
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,
|
||||||
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions, STRING_AGG(DISTINCT mg.image_url, ',') gallery,
|
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions, STRING_AGG(DISTINCT mg.image_url || ', ' || mg.featured, ' ,') gallery,
|
||||||
STRING_AGG(DISTINCT md.joining_platform_id || ', ' || md.url || ', ' || dp.short || ', ' || dp.name, ' ,') donations
|
STRING_AGG(DISTINCT md.joining_platform_id || ', ' || md.url || ', ' || dp.short || ', ' || dp.name, ' ,') donations
|
||||||
FROM mods m
|
FROM mods m
|
||||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
||||||
@@ -759,37 +772,52 @@ impl Project {
|
|||||||
slug: m.slug.clone(),
|
slug: m.slug.clone(),
|
||||||
body: m.body.clone(),
|
body: m.body.clone(),
|
||||||
follows: m.follows,
|
follows: m.follows,
|
||||||
rejection_reason: m.rejection_reason,
|
moderation_message: m.moderation_message,
|
||||||
rejection_body: m.rejection_body,
|
moderation_message_body: m.moderation_message_body,
|
||||||
},
|
},
|
||||||
project_type: m.project_type_name,
|
project_type: m.project_type_name,
|
||||||
categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(),
|
categories: m.categories.map(|x| x.split(',').map(|x| x.to_string()).collect()).unwrap_or_default(),
|
||||||
versions: m.versions.unwrap_or_default().split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect(),
|
versions: m.versions.map(|x| x.split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect()).unwrap_or_default(),
|
||||||
donation_urls: m
|
gallery_items: m
|
||||||
.donations
|
.gallery
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.split(" ,")
|
.split(" ,")
|
||||||
.map(|d| {
|
.map(|d| {
|
||||||
let strings: Vec<&str> = d.split(", ").collect();
|
let strings: Vec<&str> = d.split(", ").collect();
|
||||||
|
|
||||||
if strings.len() >= 3 {
|
if strings.len() >= 2 {
|
||||||
Some(DonationUrl {
|
Some(GalleryItem {
|
||||||
project_id: ProjectId(id),
|
project_id: ProjectId(id),
|
||||||
platform_id: DonationPlatformId(strings[0].parse().unwrap_or(0)),
|
image_url: strings[0].to_string(),
|
||||||
platform_short: strings[2].to_string(),
|
featured: strings[1].parse().unwrap_or(false)
|
||||||
platform_name: strings[3].to_string(),
|
})
|
||||||
url: strings[1].to_string(),
|
} else {
|
||||||
})
|
None
|
||||||
} else {
|
}
|
||||||
None
|
})
|
||||||
}
|
.flatten()
|
||||||
})
|
.collect(),
|
||||||
.flatten()
|
donation_urls: m
|
||||||
.collect(),
|
.donations
|
||||||
gallery_items: m.gallery.iter().map(|x| GalleryItem {
|
.unwrap_or_default()
|
||||||
project_id: ProjectId(id),
|
.split(" ,")
|
||||||
image_url: x.to_string()
|
.map(|d| {
|
||||||
}).collect(),
|
let strings: Vec<&str> = d.split(", ").collect();
|
||||||
|
|
||||||
|
if strings.len() >= 3 {
|
||||||
|
Some(DonationUrl {
|
||||||
|
project_id: ProjectId(id),
|
||||||
|
platform_id: DonationPlatformId(strings[0].parse().unwrap_or(0)),
|
||||||
|
platform_short: strings[2].to_string(),
|
||||||
|
platform_name: strings[3].to_string(),
|
||||||
|
url: strings[1].to_string(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.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,
|
||||||
|
|||||||
@@ -439,13 +439,13 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_id_from_username_or_id<'a, 'b, E>(
|
pub async fn get_id_from_username_or_id<'a, 'b, E>(
|
||||||
username_or_id: String,
|
username_or_id: &str,
|
||||||
executor: E,
|
executor: E,
|
||||||
) -> Result<Option<UserId>, sqlx::error::Error>
|
) -> Result<Option<UserId>, sqlx::error::Error>
|
||||||
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);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ pub struct Project {
|
|||||||
/// The status of the project
|
/// The status of the project
|
||||||
pub status: ProjectStatus,
|
pub status: ProjectStatus,
|
||||||
/// The rejection data of the project
|
/// The rejection data of the project
|
||||||
pub rejection_data: Option<RejectionReason>,
|
pub moderator_message: Option<ModeratorMessage>,
|
||||||
|
|
||||||
/// The license of this project
|
/// The license of this project
|
||||||
pub license: License,
|
pub license: License,
|
||||||
@@ -77,12 +77,18 @@ pub struct Project {
|
|||||||
pub donation_urls: Option<Vec<DonationLink>>,
|
pub donation_urls: Option<Vec<DonationLink>>,
|
||||||
|
|
||||||
/// A string of URLs to visual content featuring the project
|
/// A string of URLs to visual content featuring the project
|
||||||
pub gallery: Vec<String>,
|
pub gallery: Vec<GalleryItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct RejectionReason {
|
pub struct GalleryItem {
|
||||||
pub reason: String,
|
pub url: String,
|
||||||
|
pub featured: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ModeratorMessage {
|
||||||
|
pub message: String,
|
||||||
pub body: Option<String>,
|
pub body: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct TeamId(pub u64);
|
pub struct TeamId(pub u64);
|
||||||
|
|
||||||
pub const OWNER_ROLE: &str = "Owner";
|
pub const OWNER_ROLE: &str = "Owner";
|
||||||
|
pub const DEFAULT_ROLE: &str = "Member";
|
||||||
|
|
||||||
// TODO: permissions, role names, etc
|
// TODO: permissions, role names, etc
|
||||||
/// A team of users who control a project
|
/// A team of users who control a project
|
||||||
|
|||||||
@@ -181,19 +181,48 @@ pub async fn auth_callback(
|
|||||||
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?;
|
||||||
|
|
||||||
User {
|
let mut username_increment: i32 = 0;
|
||||||
id: user_id,
|
let mut username = None;
|
||||||
github_id: Some(user.id as i64),
|
|
||||||
username: user.login,
|
while username.is_none() {
|
||||||
name: user.name,
|
let test_username = format!(
|
||||||
email: user.email,
|
"{}{}",
|
||||||
avatar_url: Some(user.avatar_url),
|
&*user.login,
|
||||||
bio: user.bio,
|
if username_increment > 0 {
|
||||||
created: Utc::now(),
|
username_increment.to_string()
|
||||||
role: Role::Developer.to_string(),
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_id = crate::database::models::User::get_id_from_username_or_id(
|
||||||
|
&*test_username,
|
||||||
|
&**client,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if new_id.is_none() {
|
||||||
|
username = Some(test_username);
|
||||||
|
} else {
|
||||||
|
username_increment += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(username) = username {
|
||||||
|
User {
|
||||||
|
id: user_id,
|
||||||
|
github_id: Some(user.id as i64),
|
||||||
|
username,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
avatar_url: Some(user.avatar_url),
|
||||||
|
bio: user.bio,
|
||||||
|
created: Utc::now(),
|
||||||
|
role: Role::Developer.to_string(),
|
||||||
|
}
|
||||||
|
.insert(&mut transaction)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
.insert(&mut transaction)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,18 @@ pub fn projects_config(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(projects::project_delete)
|
.service(projects::project_delete)
|
||||||
.service(projects::project_edit)
|
.service(projects::project_edit)
|
||||||
.service(projects::project_icon_edit)
|
.service(projects::project_icon_edit)
|
||||||
|
.service(projects::delete_project_icon)
|
||||||
|
.service(projects::add_gallery_item)
|
||||||
|
.service(projects::edit_gallery_item)
|
||||||
|
.service(projects::delete_gallery_item)
|
||||||
.service(projects::project_follow)
|
.service(projects::project_follow)
|
||||||
.service(projects::project_unfollow)
|
.service(projects::project_unfollow)
|
||||||
.service(teams::team_members_get_project)
|
.service(teams::team_members_get_project)
|
||||||
.service(web::scope("{project_id}").service(versions::version_list))
|
.service(
|
||||||
.service(projects::dependency_list),
|
web::scope("{project_id}")
|
||||||
|
.service(versions::version_list)
|
||||||
|
.service(projects::dependency_list),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +119,7 @@ pub fn teams_config(cfg: &mut web::ServiceConfig) {
|
|||||||
web::scope("team")
|
web::scope("team")
|
||||||
.service(teams::team_members_get)
|
.service(teams::team_members_get)
|
||||||
.service(teams::edit_team_member)
|
.service(teams::edit_team_member)
|
||||||
|
.service(teams::transfer_ownership)
|
||||||
.service(teams::add_team_member)
|
.service(teams::add_team_member)
|
||||||
.service(teams::join_team)
|
.service(teams::join_team)
|
||||||
.service(teams::remove_team_member),
|
.service(teams::remove_team_member),
|
||||||
|
|||||||
@@ -185,7 +185,15 @@ struct ProjectCreateData {
|
|||||||
|
|
||||||
#[validate(length(max = 64))]
|
#[validate(length(max = 64))]
|
||||||
/// The multipart names of the gallery items to upload
|
/// The multipart names of the gallery items to upload
|
||||||
pub gallery_items: Option<Vec<String>>,
|
pub gallery_items: Option<Vec<NewGalleryItem>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||||
|
pub struct NewGalleryItem {
|
||||||
|
/// The name of the multipart item where the gallery media is located
|
||||||
|
pub item: String,
|
||||||
|
/// Whether the gallery item should show in search or not
|
||||||
|
pub featured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UploadedFile {
|
pub struct UploadedFile {
|
||||||
@@ -427,27 +435,23 @@ pub async fn project_create_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(gallery_items) = &project_create_data.gallery_items {
|
if let Some(gallery_items) = &project_create_data.gallery_items {
|
||||||
if
|
if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
|
||||||
gallery_items
|
|
||||||
.iter()
|
|
||||||
.find(|x| *x == name)
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
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)?);
|
const FILE_SIZE_CAP: usize = 5 * (1 << 20);
|
||||||
}
|
|
||||||
|
|
||||||
const FILE_SIZE_CAP: usize = 5 * (1 << 20);
|
if data.len() >= FILE_SIZE_CAP {
|
||||||
|
return Err(CreateError::InvalidInput(String::from(
|
||||||
if data.len() >= FILE_SIZE_CAP {
|
"Gallery image exceeds the maximum of 5MiB.",
|
||||||
return Err(CreateError::InvalidInput(String::from(
|
)));
|
||||||
"Gallery image exceeds the maximum of 5MiB.",
|
} else {
|
||||||
)));
|
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash = sha1::Sha1::from(&data).hexdigest();
|
let hash = sha1::Sha1::from(&data).hexdigest();
|
||||||
let (_, file_extension) = super::version_creation::get_name_ext(&content_disposition)?;
|
let (_, file_extension) =
|
||||||
|
super::version_creation::get_name_ext(&content_disposition)?;
|
||||||
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
||||||
.ok_or_else(|| CreateError::InvalidIconFormat(file_extension.to_string()))?;
|
.ok_or_else(|| CreateError::InvalidIconFormat(file_extension.to_string()))?;
|
||||||
|
|
||||||
@@ -461,7 +465,10 @@ pub async fn project_create_inner(
|
|||||||
file_name: upload_data.file_name.clone(),
|
file_name: upload_data.file_name.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
gallery_urls.push(format!("{}/{}", cdn_url, url));
|
gallery_urls.push(crate::models::projects::GalleryItem {
|
||||||
|
url,
|
||||||
|
featured: item.featured,
|
||||||
|
});
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -628,7 +635,8 @@ pub async fn project_create_inner(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|x| models::project_item::GalleryItem {
|
.map(|x| models::project_item::GalleryItem {
|
||||||
project_id: project_id.into(),
|
project_id: project_id.into(),
|
||||||
image_url: x.to_string(),
|
image_url: x.url.clone(),
|
||||||
|
featured: x.featured,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
@@ -647,7 +655,7 @@ pub async fn project_create_inner(
|
|||||||
published: now,
|
published: now,
|
||||||
updated: now,
|
updated: now,
|
||||||
status: status.clone(),
|
status: status.clone(),
|
||||||
rejection_data: None,
|
moderator_message: None,
|
||||||
license: License {
|
license: License {
|
||||||
id: project_create_data.license_id.clone(),
|
id: project_create_data.license_id.clone(),
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
@@ -783,13 +791,13 @@ async fn process_icon_upload(
|
|||||||
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
|
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
|
||||||
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)?);
|
if data.len() >= 262144 {
|
||||||
}
|
return Err(CreateError::InvalidInput(String::from(
|
||||||
|
"Icons must be smaller than 256KiB",
|
||||||
if data.len() >= 262144 {
|
)));
|
||||||
return Err(CreateError::InvalidInput(String::from(
|
} else {
|
||||||
"Icons must be smaller than 256KiB",
|
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
|
||||||
)));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let upload_data = file_host
|
let upload_data = file_host
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use crate::database;
|
|||||||
use crate::file_hosting::FileHost;
|
use crate::file_hosting::FileHost;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::projects::{
|
use crate::models::projects::{
|
||||||
DonationLink, License, ProjectId, ProjectStatus, RejectionReason, SearchRequest, SideType,
|
DonationLink, GalleryItem, License, ModeratorMessage, ProjectId, ProjectStatus, SearchRequest,
|
||||||
|
SideType,
|
||||||
};
|
};
|
||||||
use crate::models::teams::Permissions;
|
use crate::models::teams::Permissions;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
@@ -235,10 +236,10 @@ pub fn convert_project(
|
|||||||
published: m.published,
|
published: m.published,
|
||||||
updated: m.updated,
|
updated: m.updated,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
rejection_data: if let Some(reason) = m.rejection_reason {
|
moderator_message: if let Some(message) = m.moderation_message {
|
||||||
Some(RejectionReason {
|
Some(ModeratorMessage {
|
||||||
reason,
|
message,
|
||||||
body: m.rejection_body,
|
body: m.moderation_message_body,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -272,7 +273,10 @@ pub fn convert_project(
|
|||||||
gallery: data
|
gallery: data
|
||||||
.gallery_items
|
.gallery_items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| x.image_url)
|
.map(|x| GalleryItem {
|
||||||
|
url: x.image_url,
|
||||||
|
featured: x.featured,
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,14 +349,14 @@ pub struct EditProject {
|
|||||||
with = "::serde_with::rust::double_option"
|
with = "::serde_with::rust::double_option"
|
||||||
)]
|
)]
|
||||||
#[validate(length(max = 2000))]
|
#[validate(length(max = 2000))]
|
||||||
pub rejection_reason: Option<Option<String>>,
|
pub moderation_message: Option<Option<String>>,
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Option::is_none",
|
skip_serializing_if = "Option::is_none",
|
||||||
with = "::serde_with::rust::double_option"
|
with = "::serde_with::rust::double_option"
|
||||||
)]
|
)]
|
||||||
#[validate(length(max = 65536))]
|
#[validate(length(max = 65536))]
|
||||||
pub rejection_body: Option<Option<String>>,
|
pub moderation_message_body: Option<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("{id}")]
|
#[patch("{id}")]
|
||||||
@@ -465,7 +469,7 @@ pub async fn project_edit(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE mods
|
UPDATE mods
|
||||||
SET rejection_reason = NULL
|
SET moderation_message = NULL
|
||||||
WHERE (id = $1)
|
WHERE (id = $1)
|
||||||
",
|
",
|
||||||
id as database::models::ids::ProjectId,
|
id as database::models::ids::ProjectId,
|
||||||
@@ -476,7 +480,7 @@ pub async fn project_edit(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE mods
|
UPDATE mods
|
||||||
SET rejection_body = NULL
|
SET moderation_message_body = NULL
|
||||||
WHERE (id = $1)
|
WHERE (id = $1)
|
||||||
",
|
",
|
||||||
id as database::models::ids::ProjectId,
|
id as database::models::ids::ProjectId,
|
||||||
@@ -841,10 +845,10 @@ pub async fn project_edit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(rejection_reason) = &new_project.rejection_reason {
|
if let Some(moderation_message) = &new_project.moderation_message {
|
||||||
if !user.role.is_mod() {
|
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 rejection reason of this project!"
|
"You do not have the permissions to edit the moderation message of this project!"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -852,20 +856,20 @@ pub async fn project_edit(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE mods
|
UPDATE mods
|
||||||
SET rejection_reason = $1
|
SET moderation_message = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
rejection_reason.as_deref(),
|
moderation_message.as_deref(),
|
||||||
id as database::models::ids::ProjectId,
|
id as database::models::ids::ProjectId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(rejection_body) = &new_project.rejection_body {
|
if let Some(moderation_message_body) = &new_project.moderation_message_body {
|
||||||
if !user.role.is_mod() {
|
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 rejection body of this project!"
|
"You do not have the permissions to edit the moderation message body of this project!"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -873,10 +877,10 @@ pub async fn project_edit(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE mods
|
UPDATE mods
|
||||||
SET rejection_body = $1
|
SET moderation_message_body = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
rejection_body.as_deref(),
|
moderation_message_body.as_deref(),
|
||||||
id as database::models::ids::ProjectId,
|
id as database::models::ids::ProjectId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
@@ -971,15 +975,17 @@ pub async fn project_icon_edit(
|
|||||||
|
|
||||||
let mut bytes = web::BytesMut::new();
|
let mut bytes = web::BytesMut::new();
|
||||||
while let Some(item) = payload.next().await {
|
while let Some(item) = payload.next().await {
|
||||||
bytes.extend_from_slice(&item.map_err(|_| {
|
if bytes.len() >= 262144 {
|
||||||
ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string())
|
return Err(ApiError::InvalidInputError(String::from(
|
||||||
})?);
|
"Icons must be smaller than 256KiB",
|
||||||
}
|
)));
|
||||||
|
} else {
|
||||||
if bytes.len() >= 262144 {
|
bytes.extend_from_slice(&item.map_err(|_| {
|
||||||
return Err(ApiError::InvalidInputError(String::from(
|
ApiError::InvalidInputError(
|
||||||
"Icons must be smaller than 256KiB",
|
"Unable to parse bytes in payload sent!".to_string(),
|
||||||
)));
|
)
|
||||||
|
})?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||||
@@ -1081,10 +1087,16 @@ pub async fn delete_project_icon(
|
|||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GalleryCreateQuery {
|
||||||
|
pub featured: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[post("{id}/gallery")]
|
#[post("{id}/gallery")]
|
||||||
pub async fn add_gallery_item(
|
pub async fn add_gallery_item(
|
||||||
web::Query(ext): web::Query<Extension>,
|
web::Query(ext): web::Query<Extension>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
web::Query(item): web::Query<GalleryCreateQuery>,
|
||||||
info: web::Path<(String,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||||
@@ -1123,17 +1135,19 @@ pub async fn add_gallery_item(
|
|||||||
|
|
||||||
let mut bytes = web::BytesMut::new();
|
let mut bytes = web::BytesMut::new();
|
||||||
while let Some(item) = payload.next().await {
|
while let Some(item) = payload.next().await {
|
||||||
bytes.extend_from_slice(&item.map_err(|_| {
|
const FILE_SIZE_CAP: usize = 5 * (1 << 20);
|
||||||
ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string())
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
|
|
||||||
const FILE_SIZE_CAP: usize = 5 * (1 << 20);
|
if bytes.len() >= FILE_SIZE_CAP {
|
||||||
|
return Err(ApiError::InvalidInputError(String::from(
|
||||||
if bytes.len() >= FILE_SIZE_CAP {
|
"Gallery image exceeds the maximum of 5MiB.",
|
||||||
return Err(ApiError::InvalidInputError(String::from(
|
)));
|
||||||
"Gallery image exceeds the maximum of 5MiB.",
|
} else {
|
||||||
)));
|
bytes.extend_from_slice(&item.map_err(|_| {
|
||||||
|
ApiError::InvalidInputError(
|
||||||
|
"Unable to parse bytes in payload sent!".to_string(),
|
||||||
|
)
|
||||||
|
})?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||||
@@ -1149,6 +1163,7 @@ pub async fn add_gallery_item(
|
|||||||
database::models::project_item::GalleryItem {
|
database::models::project_item::GalleryItem {
|
||||||
project_id: project_item.id,
|
project_id: project_item.id,
|
||||||
image_url: format!("{}/{}", cdn_url, url),
|
image_url: format!("{}/{}", cdn_url, url),
|
||||||
|
featured: item.featured,
|
||||||
}
|
}
|
||||||
.insert(&mut transaction)
|
.insert(&mut transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -1163,14 +1178,93 @@ pub async fn add_gallery_item(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GalleryItem {
|
pub struct GalleryEditQuery {
|
||||||
pub item: String,
|
pub url: String,
|
||||||
|
pub featured: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("{id}/gallery")]
|
||||||
|
pub async fn edit_gallery_item(
|
||||||
|
req: HttpRequest,
|
||||||
|
web::Query(item): web::Query<GalleryEditQuery>,
|
||||||
|
info: web::Path<(String,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
|
let string = info.into_inner().0;
|
||||||
|
|
||||||
|
let project_item =
|
||||||
|
database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::InvalidInputError("The specified project does not exist!".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !user.role.is_mod() {
|
||||||
|
let team_member = database::models::TeamMember::get_from_user_id(
|
||||||
|
project_item.team_id,
|
||||||
|
user.id.into(),
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::DatabaseError)?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::InvalidInputError("The specified project does not exist!".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You don't have permission to edit this project's gallery.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
let id = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT id FROM mods_gallery
|
||||||
|
WHERE image_url = $1
|
||||||
|
",
|
||||||
|
item.url
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut *transaction)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::InvalidInputError(format!(
|
||||||
|
"Gallery item at URL {} is not part of the project's gallery.",
|
||||||
|
item.url
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.id;
|
||||||
|
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE mods_gallery
|
||||||
|
SET featured = $2
|
||||||
|
WHERE id = $1
|
||||||
|
",
|
||||||
|
id,
|
||||||
|
item.featured
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct GalleryDeleteQuery {
|
||||||
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("{id}/gallery")]
|
#[delete("{id}/gallery")]
|
||||||
pub async fn delete_gallery_item(
|
pub async fn delete_gallery_item(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
web::Query(item): web::Query<GalleryItem>,
|
web::Query(item): web::Query<GalleryDeleteQuery>,
|
||||||
info: web::Path<(String,)>,
|
info: web::Path<(String,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||||
@@ -1199,7 +1293,7 @@ pub async fn delete_gallery_item(
|
|||||||
|
|
||||||
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 gallery.".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1210,19 +1304,19 @@ pub async fn delete_gallery_item(
|
|||||||
SELECT id FROM mods_gallery
|
SELECT id FROM mods_gallery
|
||||||
WHERE image_url = $1
|
WHERE image_url = $1
|
||||||
",
|
",
|
||||||
item.item
|
item.url
|
||||||
)
|
)
|
||||||
.fetch_optional(&mut *transaction)
|
.fetch_optional(&mut *transaction)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ApiError::InvalidInputError(format!(
|
ApiError::InvalidInputError(format!(
|
||||||
"Gallery item at URL {} is not part of the project's gallery.",
|
"Gallery item at URL {} is not part of the project's gallery.",
|
||||||
item.item
|
item.url
|
||||||
))
|
))
|
||||||
})?
|
})?
|
||||||
.id;
|
.id;
|
||||||
|
|
||||||
let name = item.item.split('/').next();
|
let name = item.url.split('/').next();
|
||||||
|
|
||||||
if let Some(item_path) = name {
|
if let Some(item_path) = name {
|
||||||
file_host.delete_file_version("", item_path).await?;
|
file_host.delete_file_version("", item_path).await?;
|
||||||
|
|||||||
@@ -341,6 +341,66 @@ pub async fn edit_team_member(
|
|||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct TransferOwnership {
|
||||||
|
pub user_id: UserId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("{id}/owner")]
|
||||||
|
pub async fn transfer_ownership(
|
||||||
|
req: HttpRequest,
|
||||||
|
info: web::Path<(TeamId,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
new_owner: web::Json<TransferOwnership>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let id = info.into_inner().0;
|
||||||
|
|
||||||
|
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
|
let team_member =
|
||||||
|
TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool).await?;
|
||||||
|
|
||||||
|
let member = match team_member {
|
||||||
|
Some(m) => m,
|
||||||
|
None => {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You don't have permission to edit members of this team".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if member.role != crate::models::teams::OWNER_ROLE {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You don't have permission to edit the ownership of this team".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
TeamMember::edit_team_member(
|
||||||
|
id.into(),
|
||||||
|
current_user.id.into(),
|
||||||
|
None,
|
||||||
|
Some(crate::models::teams::DEFAULT_ROLE.to_string()),
|
||||||
|
None,
|
||||||
|
&mut transaction,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
TeamMember::edit_team_member(
|
||||||
|
id.into(),
|
||||||
|
new_owner.user_id.into(),
|
||||||
|
None,
|
||||||
|
Some(crate::models::teams::OWNER_ROLE.to_string()),
|
||||||
|
None,
|
||||||
|
&mut transaction,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
|
}
|
||||||
|
|
||||||
#[delete("{id}/members/{user_id}")]
|
#[delete("{id}/members/{user_id}")]
|
||||||
pub async fn remove_team_member(
|
pub async fn remove_team_member(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ pub async fn projects_list(
|
|||||||
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(info.into_inner().0, &**pool)
|
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(id) = id_option {
|
if let Some(id) = id_option {
|
||||||
@@ -172,7 +172,7 @@ pub async fn user_edit(
|
|||||||
.map_err(|err| 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(info.into_inner().0, &**pool)
|
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(id) = id_option {
|
if let Some(id) = id_option {
|
||||||
@@ -182,17 +182,28 @@ pub async fn user_edit(
|
|||||||
let mut transaction = pool.begin().await?;
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
if let Some(username) = &new_user.username {
|
if let Some(username) = &new_user.username {
|
||||||
sqlx::query!(
|
let user_option =
|
||||||
"
|
crate::database::models::User::get_id_from_username_or_id(username, &**pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if user_option.is_none() {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET username = $1
|
SET username = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
username,
|
username,
|
||||||
id as crate::database::models::ids::UserId,
|
id as crate::database::models::ids::UserId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
} else {
|
||||||
|
return Err(ApiError::InvalidInputError(format!(
|
||||||
|
"Username {} is taken!",
|
||||||
|
username
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = &new_user.name {
|
if let Some(name) = &new_user.name {
|
||||||
@@ -289,9 +300,11 @@ pub async fn user_icon_edit(
|
|||||||
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 =
|
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.id != id.into() && !user.role.is_mod() {
|
if user.id != id.into() && !user.role.is_mod() {
|
||||||
@@ -326,17 +339,17 @@ pub async fn user_icon_edit(
|
|||||||
|
|
||||||
let mut bytes = web::BytesMut::new();
|
let mut bytes = web::BytesMut::new();
|
||||||
while let Some(item) = payload.next().await {
|
while let Some(item) = payload.next().await {
|
||||||
bytes.extend_from_slice(&item.map_err(|_| {
|
if bytes.len() >= 262144 {
|
||||||
ApiError::InvalidInputError(
|
return Err(ApiError::InvalidInputError(String::from(
|
||||||
"Unable to parse bytes in payload sent!".to_string(),
|
"Icons must be smaller than 256KiB",
|
||||||
)
|
)));
|
||||||
})?);
|
} else {
|
||||||
}
|
bytes.extend_from_slice(&item.map_err(|_| {
|
||||||
|
ApiError::InvalidInputError(
|
||||||
if bytes.len() >= 262144 {
|
"Unable to parse bytes in payload sent!".to_string(),
|
||||||
return Err(ApiError::InvalidInputError(String::from(
|
)
|
||||||
"Icons must be smaller than 256KiB",
|
})?);
|
||||||
)));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let upload_data = file_host
|
let upload_data = file_host
|
||||||
@@ -389,7 +402,7 @@ pub async fn user_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 id_option =
|
let id_option =
|
||||||
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
|
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(id) = id_option {
|
if let Some(id) = id_option {
|
||||||
@@ -428,7 +441,7 @@ pub async fn user_follows(
|
|||||||
) -> 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(info.into_inner().0, &**pool)
|
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(id) = id_option {
|
if let Some(id) = id_option {
|
||||||
@@ -475,7 +488,7 @@ pub async fn user_notifications(
|
|||||||
) -> 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(info.into_inner().0, &**pool)
|
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(id) = id_option {
|
if let Some(id) = id_option {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub async fn mods_list(
|
|||||||
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(info.into_inner().0, &**pool)
|
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(id) = id_option {
|
if let Some(id) = id_option {
|
||||||
@@ -51,7 +51,7 @@ pub async fn user_follows(
|
|||||||
) -> 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(info.into_inner().0, &**pool)
|
crate::database::models::User::get_id_from_username_or_id(&*info.into_inner().0, &**pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(id) = id_option {
|
if let Some(id) = id_option {
|
||||||
|
|||||||
@@ -585,17 +585,16 @@ pub async fn upload_file(
|
|||||||
|
|
||||||
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)?);
|
// Project file size limit of 100MiB
|
||||||
}
|
const FILE_SIZE_CAP: usize = 100 * (1 << 20);
|
||||||
|
|
||||||
// Project file size limit of 100MiB
|
if data.len() >= FILE_SIZE_CAP {
|
||||||
const FILE_SIZE_CAP: usize = 100 * (1 << 20);
|
return Err(CreateError::InvalidInput(
|
||||||
|
String::from("Project file exceeds the maximum of 100MiB. Contact a moderator or admin to request permission to upload larger files.")
|
||||||
// TODO: override file size cap for authorized users or projects
|
));
|
||||||
if data.len() >= FILE_SIZE_CAP {
|
} else {
|
||||||
return Err(CreateError::InvalidInput(
|
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
|
||||||
String::from("Project file exceeds the maximum of 100MiB. Contact a moderator or admin to request permission to upload larger files.")
|
}
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let validation_result = validate_file(
|
let validation_result = validate_file(
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, Index
|
|||||||
m.updated updated,
|
m.updated updated,
|
||||||
m.team_id team_id, m.license license, m.slug slug,
|
m.team_id team_id, m.license license, m.slug slug,
|
||||||
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username,
|
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username,
|
||||||
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions
|
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions,
|
||||||
|
STRING_AGG(DISTINCT mg.image_url, ',') gallery
|
||||||
FROM mods m
|
FROM mods m
|
||||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
||||||
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
|
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
|
||||||
@@ -27,6 +28,7 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, Index
|
|||||||
LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id
|
LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id
|
||||||
LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id
|
LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id
|
||||||
LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id
|
LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id
|
||||||
|
LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id
|
||||||
INNER JOIN statuses s ON s.id = m.status
|
INNER JOIN statuses s ON s.id = m.status
|
||||||
INNER JOIN project_types pt ON pt.id = m.project_type
|
INNER JOIN project_types pt ON pt.id = m.project_type
|
||||||
INNER JOIN side_types cs ON m.client_side = cs.id
|
INNER JOIN side_types cs ON m.client_side = cs.id
|
||||||
@@ -43,10 +45,10 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, Index
|
|||||||
.fetch_many(&pool)
|
.fetch_many(&pool)
|
||||||
.try_filter_map(|e| async {
|
.try_filter_map(|e| async {
|
||||||
Ok(e.right().map(|m| {
|
Ok(e.right().map(|m| {
|
||||||
let mut categories = m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect::<Vec<String>>();
|
let mut categories = m.categories.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<String>>()).unwrap_or_default();
|
||||||
categories.append(&mut m.loaders.unwrap_or_default().split(',').map(|x| x.to_string()).collect::<Vec<String>>());
|
categories.append(&mut m.loaders.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<String>>()).unwrap_or_default());
|
||||||
|
|
||||||
let versions : Vec<String> = m.versions.unwrap_or_default().split(',').map(|x| x.to_string()).collect::<Vec<String>>();
|
let versions : Vec<String> = m.versions.map(|x| x.split(',').map(|x| x.to_string()).collect()).unwrap_or_default();
|
||||||
|
|
||||||
let project_id : crate::models::projects::ProjectId = ProjectId(m.id).into();
|
let project_id : crate::models::projects::ProjectId = ProjectId(m.id).into();
|
||||||
|
|
||||||
@@ -70,6 +72,7 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchProject>, Index
|
|||||||
server_side: m.server_side_type,
|
server_side: m.server_side_type,
|
||||||
slug: m.slug,
|
slug: m.slug,
|
||||||
project_type: m.project_type_name,
|
project_type: m.project_type_name,
|
||||||
|
gallery: m.gallery.map(|x| x.split(',').map(|x| x.to_string()).collect()).unwrap_or_default()
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -89,7 +92,8 @@ pub async fn query_one(
|
|||||||
m.updated updated,
|
m.updated updated,
|
||||||
m.team_id team_id, m.license license, m.slug slug,
|
m.team_id team_id, m.license license, m.slug slug,
|
||||||
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username,
|
s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username,
|
||||||
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions
|
STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions,
|
||||||
|
STRING_AGG(DISTINCT mg.image_url, ',') gallery
|
||||||
FROM mods m
|
FROM mods m
|
||||||
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id
|
||||||
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
|
LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id
|
||||||
@@ -98,6 +102,7 @@ pub async fn query_one(
|
|||||||
LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id
|
LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id
|
||||||
LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id
|
LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id
|
||||||
LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id
|
LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id
|
||||||
|
LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id
|
||||||
INNER JOIN statuses s ON s.id = m.status
|
INNER JOIN statuses s ON s.id = m.status
|
||||||
INNER JOIN project_types pt ON pt.id = m.project_type
|
INNER JOIN project_types pt ON pt.id = m.project_type
|
||||||
INNER JOIN side_types cs ON m.client_side = cs.id
|
INNER JOIN side_types cs ON m.client_side = cs.id
|
||||||
@@ -116,25 +121,19 @@ pub async fn query_one(
|
|||||||
|
|
||||||
let mut categories = m
|
let mut categories = m
|
||||||
.categories
|
.categories
|
||||||
.unwrap_or_default()
|
.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<String>>())
|
||||||
.split(',')
|
.unwrap_or_default();
|
||||||
.map(|x| x.to_string())
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
categories.append(
|
categories.append(
|
||||||
&mut m
|
&mut m
|
||||||
.loaders
|
.loaders
|
||||||
.unwrap_or_default()
|
.map(|x| x.split(',').map(|x| x.to_string()).collect::<Vec<String>>())
|
||||||
.split(',')
|
.unwrap_or_default(),
|
||||||
.map(|x| x.to_string())
|
|
||||||
.collect::<Vec<String>>(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let versions: Vec<String> = m
|
let versions: Vec<String> = m
|
||||||
.versions
|
.versions
|
||||||
.unwrap_or_default()
|
.map(|x| x.split(',').map(|x| x.to_string()).collect())
|
||||||
.split(',')
|
.unwrap_or_default();
|
||||||
.map(|x| x.to_string())
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
let project_id: crate::models::projects::ProjectId = ProjectId(m.id).into();
|
let project_id: crate::models::projects::ProjectId = ProjectId(m.id).into();
|
||||||
|
|
||||||
@@ -161,5 +160,9 @@ pub async fn query_one(
|
|||||||
server_side: m.server_side_type,
|
server_side: m.server_side_type,
|
||||||
slug: m.slug,
|
slug: m.slug,
|
||||||
project_type: m.project_type_name,
|
project_type: m.project_type_name,
|
||||||
|
gallery: m
|
||||||
|
.gallery
|
||||||
|
.map(|x| x.split(',').map(|x| x.to_string()).collect())
|
||||||
|
.unwrap_or_default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ pub struct UploadSearchProject {
|
|||||||
pub license: String,
|
pub license: String,
|
||||||
pub client_side: String,
|
pub client_side: String,
|
||||||
pub server_side: String,
|
pub server_side: String,
|
||||||
|
pub gallery: Vec<String>,
|
||||||
|
|
||||||
/// RFC 3339 formatted creation date of the project
|
/// RFC 3339 formatted creation date of the project
|
||||||
pub date_created: DateTime<Utc>,
|
pub date_created: DateTime<Utc>,
|
||||||
@@ -117,6 +118,7 @@ pub struct ResultSearchProject {
|
|||||||
pub license: String,
|
pub license: String,
|
||||||
pub client_side: String,
|
pub client_side: String,
|
||||||
pub server_side: String,
|
pub server_side: String,
|
||||||
|
pub gallery: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Document for UploadSearchProject {
|
impl Document for UploadSearchProject {
|
||||||
|
|||||||
Reference in New Issue
Block a user