Refactor mod creation route, add more checks (#80)

This also removes the `team_members` field of `InitialModData`, as
team members are no longer specified at mod creation.
This commit is contained in:
Aeledfyr
2020-10-17 21:34:23 -05:00
committed by GitHub
parent 520b12e56b
commit d0fb5c3bd5
6 changed files with 730 additions and 362 deletions

View File

@@ -19,7 +19,7 @@ pub struct InitialVersionData {
pub file_parts: Vec<String>,
pub version_number: String,
pub version_title: String,
pub version_body: String,
pub version_body: Option<String>,
pub dependencies: Vec<VersionId>,
pub game_versions: Vec<GameVersion>,
pub release_channel: VersionType,
@@ -31,6 +31,45 @@ struct InitialFileData {
// TODO: hashes?
}
pub fn check_version(version: &InitialVersionData) -> Result<(), CreateError> {
/*
# InitialVersionData
file_parts: Vec<String>, 1..=256
version_number: 1..=64,
version_title: 3..=256,
version_body: max of 64KiB,
game_versions: Vec<GameVersion>, 1..=256
release_channel: VersionType,
loaders: Vec<ModLoader>, 1..=256
*/
use super::mod_creation::check_length;
version
.file_parts
.iter()
.map(|f| check_length(1..=256, "file part name", f))
.collect::<Result<_, _>>()?;
check_length(1..=64, "version number", &version.version_number)?;
check_length(3..=256, "version title", &version.version_title)?;
if let Some(body) = &version.version_body {
check_length(..65536, "version body", body)?;
}
version
.game_versions
.iter()
.map(|v| check_length(1..=256, "game version", &v.0))
.collect::<Result<_, _>>()?;
version
.loaders
.iter()
.map(|l| check_length(1..=256, "loader name", &l.0))
.collect::<Result<_, _>>()?;
Ok(())
}
// under `/api/v1/mod/{mod_id}`
#[post("version")]
pub async fn version_create(
@@ -105,8 +144,11 @@ async fn version_create_inner(
return Err(CreateError::MissingValueError("Missing mod id".to_string()));
}
check_version(version_create_data)?;
let mod_id: models::ModId = version_create_data.mod_id.unwrap().into();
// Ensure that the mod this version is being added to exists
let results = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
mod_id as models::ModId
@@ -120,6 +162,8 @@ async fn version_create_inner(
));
}
// Check whether there is already a version of this mod with the
// same version number
let results = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM versions WHERE (version_number=$1) AND (mod_id=$2))",
version_create_data.version_number,
@@ -134,58 +178,61 @@ async fn version_create_inner(
));
}
let team_id = sqlx::query!(
"SELECT team_id FROM mods WHERE id=$1",
// Check that the user creating this version is a team member
// of the mod the version is being added to.
let member_ids = sqlx::query!(
"
SELECT user_id FROM team_members tm
INNER JOIN mods ON mods.team_id = tm.team_id
WHERE mods.id = $1
",
mod_id as models::ModId,
)
.fetch_one(&mut *transaction)
.await?
.team_id;
.fetch_all(&mut *transaction)
.await?;
let member_ids_rows =
sqlx::query!("SELECT user_id FROM team_members WHERE team_id=$1", team_id,)
.fetch_all(&mut *transaction)
.await?;
let member_ids: Vec<models::UserId> = member_ids
.iter()
.map(|m| models::UserId(m.user_id))
.collect();
let member_ids: Vec<i64> = member_ids_rows.iter().map(|m| m.user_id).collect();
if !member_ids.contains(&(user.id.0 as i64)) {
if !member_ids.contains(&user.id.into()) {
// TODO: Some team members may not have the permissions
// to upload mods; We need a more in depth permissions
// system.
return Err(CreateError::InvalidInput("Unauthorized".to_string()));
}
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
let body_url = format!(
"data/{}/changelogs/{}/body.md",
version_create_data.mod_id.unwrap(),
version_id
);
let uploaded_text = file_host
.upload_file(
"text/plain",
&body_url,
version_create_data.version_body.clone().into_bytes(),
)
.await?;
let body_path;
uploaded_files.push(UploadedFile {
file_id: uploaded_text.file_id.clone(),
file_name: uploaded_text.file_name.clone(),
});
if let Some(body) = &version_create_data.version_body {
let path = format!(
"data/{}/changelogs/{}/body.md",
version_create_data.mod_id.unwrap(),
version_id
);
let release_channel = models::ChannelId(
sqlx::query!(
"
SELECT id
FROM release_channels
WHERE channel = $1
",
version_create_data.release_channel.to_string()
)
.fetch_one(&mut *transaction)
.await?
.id,
);
let uploaded_text = file_host
.upload_file("text/plain", &path, body.clone().into_bytes())
.await?;
uploaded_files.push(UploadedFile {
file_id: uploaded_text.file_id.clone(),
file_name: uploaded_text.file_name.clone(),
});
body_path = Some(path);
} else {
body_path = None;
}
let release_channel = models::ChannelId::get_id(
version_create_data.release_channel.as_str(),
&mut *transaction,
)
.await?
.expect("Release channel not found in database");
let mut game_versions = Vec::with_capacity(version_create_data.game_versions.len());
for v in &version_create_data.game_versions {
@@ -209,8 +256,8 @@ async fn version_create_inner(
author_id: user.id.into(),
name: version_create_data.version_title.clone(),
version_number: version_create_data.version_number.clone(),
changelog_url: Some(format!("{}/{}", cdn_url, body_url)),
files: Vec::with_capacity(1),
changelog_url: body_path.map(|path| format!("{}/{}", cdn_url, path)),
files: Vec::new(),
dependencies: version_create_data
.dependencies
.iter()
@@ -243,22 +290,22 @@ async fn version_create_inner(
version.files.push(file_builder);
}
let version_data_safe = initial_version_data
let version_data = initial_version_data
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
let version_builder_safe = version_builder
let builder = version_builder
.ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?;
let response = Version {
id: version_builder_safe.version_id.into(),
mod_id: version_builder_safe.mod_id.into(),
id: builder.version_id.into(),
mod_id: builder.mod_id.into(),
author_id: user.id,
name: version_builder_safe.name.clone(),
version_number: version_builder_safe.version_number.clone(),
changelog_url: version_builder_safe.changelog_url.clone(),
name: builder.name.clone(),
version_number: builder.version_number.clone(),
changelog_url: builder.changelog_url.clone(),
date_published: chrono::Utc::now(),
downloads: 0,
version_type: version_data_safe.release_channel,
files: version_builder_safe
version_type: version_data.release_channel,
files: builder
.files
.iter()
.map(|file| VersionFile {
@@ -280,12 +327,12 @@ async fn version_create_inner(
filename: file.filename.clone(),
})
.collect::<Vec<_>>(),
dependencies: version_data_safe.dependencies,
game_versions: version_data_safe.game_versions,
loaders: version_data_safe.loaders,
dependencies: version_data.dependencies,
game_versions: version_data.game_versions,
loaders: version_data.loaders,
};
version_builder_safe.insert(transaction).await?;
builder.insert(transaction).await?;
Ok(HttpResponse::Ok().json(response))
}
@@ -449,6 +496,16 @@ pub async fn upload_file(
data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?);
}
// Mod file size limit of 25MiB
const FILE_SIZE_CAP: usize = 25 * (2 << 30);
// TODO: override file size cap for authorized users or mods
if data.len() >= FILE_SIZE_CAP {
return Err(CreateError::InvalidInput(
String::from("Mod file exceeds the maximum of 25MiB. Contact a moderator or admin to request permission to upload larger files.")
));
}
let upload_data = file_host
.upload_file(
content_type,