Moderation + Mod Editing (#101)
* Moderation + Mod Editing WIP * Run prepare, fix perms * Make it compile * Finish moderation and edit routes * More fixes * Use better queries * Final Fixes
This commit is contained in:
parent
da911bfeb8
commit
0500994def
70
Cargo.lock
generated
70
Cargo.lock
generated
@ -950,6 +950,41 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "3.11.10"
|
version = "3.11.10"
|
||||||
@ -1509,6 +1544,12 @@ dependencies = [
|
|||||||
"tokio-tls",
|
"tokio-tls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -1627,6 +1668,7 @@ dependencies = [
|
|||||||
"rust-s3",
|
"rust-s3",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"sqlx-macros",
|
"sqlx-macros",
|
||||||
@ -2397,6 +2439,28 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8bac272128fb3b1e98872dca27a05c18d8b78b9bd089d3edb7b5871501b50bce"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_with_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c747a9ab2e833b807f74f6b6141530655010bfa9c9c06d5508bce75c8f8072f"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha-1"
|
name = "sha-1"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@ -2635,6 +2699,12 @@ dependencies = [
|
|||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
|
|||||||
@ -23,6 +23,7 @@ reqwest = { version = "0.10.8", features = ["json"] }
|
|||||||
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_with = "1.5.1"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
rand = "0.7.3"
|
rand = "0.7.3"
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
|
|||||||
7
migrations/20201112052516_moderation.sql
Normal file
7
migrations/20201112052516_moderation.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
DELETE FROM release_channels WHERE channel = 'release-hidden';
|
||||||
|
DELETE FROM release_channels WHERE channel = 'beta-hidden';
|
||||||
|
DELETE FROM release_channels WHERE channel = 'alpha-hidden';
|
||||||
|
|
||||||
|
ALTER TABLE versions
|
||||||
|
ADD COLUMN accepted BOOLEAN NOT NULL default FALSE;
|
||||||
1023
sqlx-data.json
1023
sqlx-data.json
File diff suppressed because it is too large
Load Diff
@ -95,9 +95,10 @@ where
|
|||||||
{
|
{
|
||||||
let user = get_user_from_headers(headers, executor).await?;
|
let user = get_user_from_headers(headers, executor).await?;
|
||||||
|
|
||||||
match user.role {
|
if user.role.is_mod() {
|
||||||
Role::Moderator | Role::Admin => Ok(user),
|
Ok(user)
|
||||||
_ => Err(AuthenticationError::InvalidCredentialsError),
|
} else {
|
||||||
|
Err(AuthenticationError::InvalidCredentialsError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -80,6 +80,7 @@ impl VersionBuilder {
|
|||||||
date_published: chrono::Utc::now(),
|
date_published: chrono::Utc::now(),
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
release_channel: self.release_channel,
|
release_channel: self.release_channel,
|
||||||
|
accepted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
version.insert(&mut *transaction).await?;
|
version.insert(&mut *transaction).await?;
|
||||||
@ -152,6 +153,7 @@ pub struct Version {
|
|||||||
pub date_published: chrono::DateTime<chrono::Utc>,
|
pub date_published: chrono::DateTime<chrono::Utc>,
|
||||||
pub downloads: i32,
|
pub downloads: i32,
|
||||||
pub release_channel: ChannelId,
|
pub release_channel: ChannelId,
|
||||||
|
pub accepted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Version {
|
impl Version {
|
||||||
@ -164,12 +166,13 @@ impl Version {
|
|||||||
INSERT INTO versions (
|
INSERT INTO versions (
|
||||||
id, mod_id, author_id, name, version_number,
|
id, mod_id, author_id, name, version_number,
|
||||||
changelog_url, date_published,
|
changelog_url, date_published,
|
||||||
downloads, release_channel
|
downloads, release_channel, accepted
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6, $7,
|
$6, $7,
|
||||||
$8, $9
|
$8, $9,
|
||||||
|
$10
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as VersionId,
|
self.id as VersionId,
|
||||||
@ -181,6 +184,7 @@ impl Version {
|
|||||||
self.date_published,
|
self.date_published,
|
||||||
self.downloads,
|
self.downloads,
|
||||||
self.release_channel as ChannelId,
|
self.release_channel as ChannelId,
|
||||||
|
self.accepted
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@ -291,6 +295,15 @@ impl Version {
|
|||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM dependencies WHERE dependent_id = $1
|
||||||
|
",
|
||||||
|
id as VersionId,
|
||||||
|
)
|
||||||
|
.execute(exec)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(Some(()))
|
Ok(Some(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,7 +367,7 @@ impl Version {
|
|||||||
"
|
"
|
||||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||||
v.changelog_url, v.date_published, v.downloads,
|
v.changelog_url, v.date_published, v.downloads,
|
||||||
v.release_channel
|
v.release_channel, v.accepted
|
||||||
FROM versions v
|
FROM versions v
|
||||||
WHERE v.id = $1
|
WHERE v.id = $1
|
||||||
",
|
",
|
||||||
@ -374,6 +387,7 @@ impl Version {
|
|||||||
date_published: row.date_published,
|
date_published: row.date_published,
|
||||||
downloads: row.downloads,
|
downloads: row.downloads,
|
||||||
release_channel: ChannelId(row.release_channel),
|
release_channel: ChannelId(row.release_channel),
|
||||||
|
accepted: row.accepted,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@ -394,7 +408,7 @@ impl Version {
|
|||||||
"
|
"
|
||||||
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,
|
||||||
v.changelog_url, v.date_published, v.downloads,
|
v.changelog_url, v.date_published, v.downloads,
|
||||||
v.release_channel
|
v.release_channel, accepted
|
||||||
FROM versions v
|
FROM versions v
|
||||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||||
",
|
",
|
||||||
@ -412,6 +426,7 @@ impl Version {
|
|||||||
date_published: v.date_published,
|
date_published: v.date_published,
|
||||||
downloads: v.downloads,
|
downloads: v.downloads,
|
||||||
release_channel: ChannelId(v.release_channel),
|
release_channel: ChannelId(v.release_channel),
|
||||||
|
accepted: v.accepted,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<Version>>()
|
.try_collect::<Vec<Version>>()
|
||||||
@ -431,7 +446,7 @@ impl Version {
|
|||||||
"
|
"
|
||||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||||
v.changelog_url, v.date_published, v.downloads,
|
v.changelog_url, v.date_published, v.downloads,
|
||||||
release_channels.channel
|
release_channels.channel, v.accepted
|
||||||
FROM versions v
|
FROM versions v
|
||||||
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
||||||
WHERE v.id = $1
|
WHERE v.id = $1
|
||||||
@ -519,6 +534,7 @@ impl Version {
|
|||||||
files,
|
files,
|
||||||
loaders,
|
loaders,
|
||||||
game_versions,
|
game_versions,
|
||||||
|
accepted: row.accepted,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@ -570,6 +586,7 @@ pub struct QueryVersion {
|
|||||||
pub files: Vec<QueryFile>,
|
pub files: Vec<QueryFile>,
|
||||||
pub game_versions: Vec<String>,
|
pub game_versions: Vec<String>,
|
||||||
pub loaders: Vec<String>,
|
pub loaders: Vec<String>,
|
||||||
|
pub accepted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct QueryFile {
|
pub struct QueryFile {
|
||||||
|
|||||||
@ -58,7 +58,7 @@ pub struct Mod {
|
|||||||
/// Draft - Mod is not displayed on search, and not accessible by URL
|
/// Draft - Mod is not displayed on search, and not accessible by URL
|
||||||
/// Unlisted - Mod is not displayed on search, but accessible by URL
|
/// Unlisted - Mod is not displayed on search, but accessible by URL
|
||||||
/// Processing - Mod is not displayed on search, and not accessible by URL (Temporary state, mod under review)
|
/// Processing - Mod is not displayed on search, and not accessible by URL (Temporary state, mod under review)
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ModStatus {
|
pub enum ModStatus {
|
||||||
Approved,
|
Approved,
|
||||||
@ -103,6 +103,24 @@ impl ModStatus {
|
|||||||
ModStatus::Unknown => "unknown",
|
ModStatus::Unknown => "unknown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_hidden(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ModStatus::Approved => false,
|
||||||
|
ModStatus::Rejected => true,
|
||||||
|
ModStatus::Draft => true,
|
||||||
|
ModStatus::Unlisted => false,
|
||||||
|
ModStatus::Processing => true,
|
||||||
|
ModStatus::Unknown => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_searchable(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ModStatus::Approved => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A specific version of a mod
|
/// A specific version of a mod
|
||||||
|
|||||||
@ -45,4 +45,11 @@ impl Role {
|
|||||||
_ => Role::Developer,
|
_ => Role::Developer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_mod(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Role::Developer => false,
|
||||||
|
Role::Moderator | Role::Admin => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ use actix_web::web;
|
|||||||
mod auth;
|
mod auth;
|
||||||
mod index;
|
mod index;
|
||||||
mod mod_creation;
|
mod mod_creation;
|
||||||
|
mod moderation;
|
||||||
mod mods;
|
mod mods;
|
||||||
mod not_found;
|
mod not_found;
|
||||||
mod tags;
|
mod tags;
|
||||||
@ -16,6 +17,7 @@ pub use tags::config as tags_config;
|
|||||||
|
|
||||||
pub use self::index::index_get;
|
pub use self::index::index_get;
|
||||||
pub use self::not_found::not_found;
|
pub use self::not_found::not_found;
|
||||||
|
use crate::file_hosting::FileHostingError;
|
||||||
|
|
||||||
pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(mods::mod_search);
|
cfg.service(mods::mod_search);
|
||||||
@ -26,6 +28,7 @@ pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
|||||||
web::scope("mod")
|
web::scope("mod")
|
||||||
.service(mods::mod_get)
|
.service(mods::mod_get)
|
||||||
.service(mods::mod_delete)
|
.service(mods::mod_delete)
|
||||||
|
.service(mods::mod_edit)
|
||||||
.service(web::scope("{mod_id}").service(versions::version_list)),
|
.service(web::scope("{mod_id}").service(versions::version_list)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -37,7 +40,13 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
|||||||
web::scope("version")
|
web::scope("version")
|
||||||
.service(versions::version_get)
|
.service(versions::version_get)
|
||||||
.service(versions::version_delete)
|
.service(versions::version_delete)
|
||||||
.service(version_creation::upload_file_to_version),
|
.service(version_creation::upload_file_to_version)
|
||||||
|
.service(versions::version_edit),
|
||||||
|
);
|
||||||
|
cfg.service(
|
||||||
|
web::scope("version_file")
|
||||||
|
.service(versions::delete_file)
|
||||||
|
.service(versions::get_version_from_hash),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +76,8 @@ pub fn teams_config(cfg: &mut web::ServiceConfig) {
|
|||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum ApiError {
|
pub enum ApiError {
|
||||||
|
#[error("Error while uploading file")]
|
||||||
|
FileHostingError(#[from] FileHostingError),
|
||||||
#[error("Internal server error")]
|
#[error("Internal server error")]
|
||||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||||
#[error("Deserialization error: {0}")]
|
#[error("Deserialization error: {0}")]
|
||||||
@ -89,6 +100,7 @@ impl actix_web::ResponseError for ApiError {
|
|||||||
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||||
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(..) => 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::InvalidInputError(..) => actix_web::http::StatusCode::BAD_REQUEST,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,6 +114,7 @@ impl actix_web::ResponseError for ApiError {
|
|||||||
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
||||||
ApiError::JsonError(..) => "json_error",
|
ApiError::JsonError(..) => "json_error",
|
||||||
ApiError::SearchError(..) => "search_error",
|
ApiError::SearchError(..) => "search_error",
|
||||||
|
ApiError::FileHostingError(..) => "file_hosting_error",
|
||||||
ApiError::InvalidInputError(..) => "invalid_input",
|
ApiError::InvalidInputError(..) => "invalid_input",
|
||||||
},
|
},
|
||||||
description: &self.to_string(),
|
description: &self.to_string(),
|
||||||
|
|||||||
@ -113,6 +113,8 @@ struct ModCreateData {
|
|||||||
pub source_url: Option<String>,
|
pub source_url: Option<String>,
|
||||||
/// An optional link to the mod's wiki page or other relevant information.
|
/// An optional link to the mod's wiki page or other relevant information.
|
||||||
pub wiki_url: Option<String>,
|
pub wiki_url: Option<String>,
|
||||||
|
/// An optional boolean. If true, the mod will be created as a draft.
|
||||||
|
pub is_draft: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UploadedFile {
|
pub struct UploadedFile {
|
||||||
@ -277,6 +279,12 @@ async fn mod_create_inner(
|
|||||||
check_length(3..=2048, "mod description", &create_data.mod_description)?;
|
check_length(3..=2048, "mod description", &create_data.mod_description)?;
|
||||||
check_length(..65536, "mod body", &create_data.mod_body)?;
|
check_length(..65536, "mod body", &create_data.mod_body)?;
|
||||||
|
|
||||||
|
if create_data.categories.len() > 3 {
|
||||||
|
return Err(CreateError::InvalidInput(
|
||||||
|
"The maximum number of categories for a mod is four.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
create_data
|
create_data
|
||||||
.categories
|
.categories
|
||||||
.iter()
|
.iter()
|
||||||
@ -444,7 +452,13 @@ async fn mod_create_inner(
|
|||||||
|
|
||||||
let team_id = team.insert(&mut *transaction).await?;
|
let team_id = team.insert(&mut *transaction).await?;
|
||||||
|
|
||||||
let status = ModStatus::Processing;
|
let status;
|
||||||
|
if mod_create_data.is_draft.unwrap_or(false) {
|
||||||
|
status = ModStatus::Draft;
|
||||||
|
} else {
|
||||||
|
status = ModStatus::Processing;
|
||||||
|
}
|
||||||
|
|
||||||
let status_id = models::StatusId::get_id(&status, &mut *transaction)
|
let status_id = models::StatusId::get_id(&status, &mut *transaction)
|
||||||
.await?
|
.await?
|
||||||
.expect("No database entry found for status");
|
.expect("No database entry found for status");
|
||||||
|
|||||||
112
src/routes/moderation.rs
Normal file
112
src/routes/moderation.rs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
use super::ApiError;
|
||||||
|
use crate::auth::check_is_moderator_from_headers;
|
||||||
|
use crate::database;
|
||||||
|
use crate::models;
|
||||||
|
use crate::models::mods::{ModStatus, VersionType};
|
||||||
|
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ResultCount {
|
||||||
|
#[serde(default = "default_count")]
|
||||||
|
count: i16,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_count() -> i16 {
|
||||||
|
100
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("mods")]
|
||||||
|
pub async fn mods(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
count: web::Query<ResultCount>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
check_is_moderator_from_headers(req.headers(), &**pool).await?;
|
||||||
|
|
||||||
|
use futures::stream::TryStreamExt;
|
||||||
|
|
||||||
|
let mods = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT * FROM mods
|
||||||
|
WHERE status = (
|
||||||
|
SELECT id FROM statuses WHERE status = $1
|
||||||
|
)
|
||||||
|
ORDER BY updated ASC
|
||||||
|
LIMIT $2;
|
||||||
|
",
|
||||||
|
ModStatus::Processing.as_str(),
|
||||||
|
count.count as i64
|
||||||
|
)
|
||||||
|
.fetch_many(&**pool)
|
||||||
|
.try_filter_map(|e| async {
|
||||||
|
Ok(e.right().map(|m| models::mods::Mod {
|
||||||
|
id: database::models::ids::ModId(m.id).into(),
|
||||||
|
team: database::models::ids::TeamId(m.team_id).into(),
|
||||||
|
title: m.title,
|
||||||
|
description: m.description,
|
||||||
|
body_url: m.body_url,
|
||||||
|
published: m.published,
|
||||||
|
categories: vec![],
|
||||||
|
versions: vec![],
|
||||||
|
icon_url: m.icon_url,
|
||||||
|
issues_url: m.issues_url,
|
||||||
|
source_url: m.source_url,
|
||||||
|
status: ModStatus::Processing,
|
||||||
|
updated: m.updated,
|
||||||
|
downloads: m.downloads as u32,
|
||||||
|
wiki_url: m.wiki_url,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.try_collect::<Vec<models::mods::Mod>>()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(mods))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of versions that need to be approved
|
||||||
|
#[get("versions")]
|
||||||
|
pub async fn versions(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
count: web::Query<ResultCount>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
check_is_moderator_from_headers(req.headers(), &**pool).await?;
|
||||||
|
|
||||||
|
use futures::stream::TryStreamExt;
|
||||||
|
|
||||||
|
let versions = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT * FROM versions
|
||||||
|
WHERE accepted = FALSE
|
||||||
|
ORDER BY date_published ASC
|
||||||
|
LIMIT $1;
|
||||||
|
",
|
||||||
|
count.count as i64
|
||||||
|
)
|
||||||
|
.fetch_many(&**pool)
|
||||||
|
.try_filter_map(|e| async {
|
||||||
|
Ok(e.right().map(|m| models::mods::Version {
|
||||||
|
id: database::models::ids::VersionId(m.id).into(),
|
||||||
|
mod_id: database::models::ids::ModId(m.mod_id).into(),
|
||||||
|
author_id: database::models::ids::UserId(m.author_id).into(),
|
||||||
|
name: m.name,
|
||||||
|
version_number: m.version_number,
|
||||||
|
changelog_url: m.changelog_url,
|
||||||
|
date_published: m.date_published,
|
||||||
|
downloads: m.downloads as u32,
|
||||||
|
version_type: VersionType::Release,
|
||||||
|
files: vec![],
|
||||||
|
dependencies: vec![],
|
||||||
|
game_versions: vec![],
|
||||||
|
loaders: vec![],
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.try_collect::<Vec<models::mods::Version>>()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(versions))
|
||||||
|
}
|
||||||
@ -1,14 +1,15 @@
|
|||||||
use super::ApiError;
|
use super::ApiError;
|
||||||
use crate::auth::get_user_from_headers;
|
use crate::auth::get_user_from_headers;
|
||||||
use crate::database;
|
use crate::database;
|
||||||
|
use crate::file_hosting::FileHost;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::mods::SearchRequest;
|
use crate::models::mods::{ModStatus, SearchRequest};
|
||||||
use crate::models::teams::Permissions;
|
use crate::models::teams::Permissions;
|
||||||
use crate::models::users::Role;
|
|
||||||
use crate::search::{search_for_mod, SearchConfig, SearchError};
|
use crate::search::{search_for_mod, SearchConfig, SearchError};
|
||||||
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[get("mod")]
|
#[get("mod")]
|
||||||
pub async fn mod_search(
|
pub async fn mod_search(
|
||||||
@ -27,6 +28,7 @@ pub struct ModIds {
|
|||||||
// TODO: Make this return the full mod struct
|
// TODO: Make this return the full mod struct
|
||||||
#[get("mods")]
|
#[get("mods")]
|
||||||
pub async fn mods_get(
|
pub async fn mods_get(
|
||||||
|
req: HttpRequest,
|
||||||
web::Query(ids): web::Query<ModIds>,
|
web::Query(ids): web::Query<ModIds>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
@ -39,17 +41,48 @@ pub async fn mods_get(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
let mods = mods_data
|
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||||
.into_iter()
|
|
||||||
.filter_map(|m| m)
|
let mut mods = Vec::new();
|
||||||
.map(convert_mod)
|
|
||||||
.collect::<Vec<_>>();
|
for mod_data_option in mods_data {
|
||||||
|
if let Some(mod_data) = mod_data_option {
|
||||||
|
let mut authorized = !mod_data.status.is_hidden();
|
||||||
|
|
||||||
|
if let Some(user) = &user_option {
|
||||||
|
if !authorized {
|
||||||
|
if user.role.is_mod() {
|
||||||
|
authorized = true;
|
||||||
|
} else {
|
||||||
|
let user_id: database::models::ids::UserId = user.id.into();
|
||||||
|
|
||||||
|
let mod_exists = sqlx::query!(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM team_members WHERE id = $1 AND user_id = $2)",
|
||||||
|
mod_data.inner.team_id as database::models::ids::TeamId,
|
||||||
|
user_id as database::models::ids::UserId,
|
||||||
|
)
|
||||||
|
.fetch_one(&**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||||
|
.exists;
|
||||||
|
|
||||||
|
authorized = mod_exists.unwrap_or(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if authorized {
|
||||||
|
mods.push(convert_mod(mod_data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(mods))
|
Ok(HttpResponse::Ok().json(mods))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("{id}")]
|
#[get("{id}")]
|
||||||
pub async fn mod_get(
|
pub async fn mod_get(
|
||||||
|
req: HttpRequest,
|
||||||
info: web::Path<(models::ids::ModId,)>,
|
info: web::Path<(models::ids::ModId,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
@ -57,9 +90,38 @@ pub async fn mod_get(
|
|||||||
let mod_data = database::models::Mod::get_full(id.into(), &**pool)
|
let mod_data = database::models::Mod::get_full(id.into(), &**pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||||
|
|
||||||
if let Some(data) = mod_data {
|
if let Some(data) = mod_data {
|
||||||
Ok(HttpResponse::Ok().json(convert_mod(data)))
|
let mut authorized = !data.status.is_hidden();
|
||||||
|
|
||||||
|
if let Some(user) = user_option {
|
||||||
|
if !authorized {
|
||||||
|
if user.role.is_mod() {
|
||||||
|
authorized = true;
|
||||||
|
} else {
|
||||||
|
let user_id: database::models::ids::UserId = user.id.into();
|
||||||
|
|
||||||
|
let mod_exists = sqlx::query!(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM team_members WHERE id = $1 AND user_id = $2)",
|
||||||
|
data.inner.team_id as database::models::ids::TeamId,
|
||||||
|
user_id as database::models::ids::UserId,
|
||||||
|
)
|
||||||
|
.fetch_one(&**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||||
|
.exists;
|
||||||
|
|
||||||
|
authorized = mod_exists.unwrap_or(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if authorized {
|
||||||
|
return Ok(HttpResponse::Ok().json(convert_mod(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
@ -87,6 +149,309 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A mod returned from the API
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct EditMod {
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub body: Option<String>,
|
||||||
|
pub status: Option<ModStatus>,
|
||||||
|
pub categories: Option<Vec<String>>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub issues_url: Option<Option<String>>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub source_url: Option<Option<String>>,
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "::serde_with::rust::double_option"
|
||||||
|
)]
|
||||||
|
pub wiki_url: Option<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("{id}")]
|
||||||
|
pub async fn mod_edit(
|
||||||
|
req: HttpRequest,
|
||||||
|
info: web::Path<(models::ids::ModId,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
config: web::Data<SearchConfig>,
|
||||||
|
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||||
|
new_mod: web::Json<EditMod>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
|
|
||||||
|
let mod_id = info.into_inner().0;
|
||||||
|
let id = mod_id.into();
|
||||||
|
|
||||||
|
let result = database::models::Mod::get_full(id, &**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if let Some(mod_item) = result {
|
||||||
|
let team_member = database::models::TeamMember::get_from_user_id(
|
||||||
|
mod_item.inner.team_id,
|
||||||
|
user.id.into(),
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let permissions;
|
||||||
|
|
||||||
|
if let Some(member) = team_member {
|
||||||
|
permissions = Some(member.permissions)
|
||||||
|
} else if user.role.is_mod() {
|
||||||
|
permissions = Some(Permissions::ALL)
|
||||||
|
} else {
|
||||||
|
permissions = None
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(perms) = permissions {
|
||||||
|
let mut transaction = pool
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if let Some(title) = &new_mod.title {
|
||||||
|
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the title of this mod!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE mods
|
||||||
|
SET title = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
title,
|
||||||
|
id as database::models::ids::ModId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(description) = &new_mod.description {
|
||||||
|
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the description of this mod!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE mods
|
||||||
|
SET description = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
description,
|
||||||
|
id as database::models::ids::ModId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(status) = &new_mod.status {
|
||||||
|
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the status of this mod!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == &ModStatus::Rejected || status == &ModStatus::Approved {
|
||||||
|
if !user.role.is_mod() {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You don't have permission to set this status".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_id = database::models::StatusId::get_id(&status, &mut *transaction)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::InvalidInputError(
|
||||||
|
"No database entry for status provided.".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE mods
|
||||||
|
SET status = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
status_id as database::models::ids::StatusId,
|
||||||
|
id as database::models::ids::ModId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if mod_item.status.is_searchable() && status.is_searchable() {
|
||||||
|
delete_from_index(id.into(), config).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(categories) = &new_mod.categories {
|
||||||
|
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the categories of this mod!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if categories.len() > 3 {
|
||||||
|
return Err(ApiError::InvalidInputError(
|
||||||
|
"The maximum number of categories for a mod is four.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM mods_categories
|
||||||
|
WHERE joining_mod_id = $1
|
||||||
|
",
|
||||||
|
id as database::models::ids::ModId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
for category in categories {
|
||||||
|
let category_id = database::models::categories::Category::get_id(
|
||||||
|
&category,
|
||||||
|
&mut *transaction,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::InvalidInputError(format!(
|
||||||
|
"Category {} does not exist.",
|
||||||
|
category.clone()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
INSERT INTO mods_categories (joining_mod_id, joining_category_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
",
|
||||||
|
id as database::models::ids::ModId,
|
||||||
|
category_id as database::models::ids::CategoryId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(issues_url) = &new_mod.issues_url {
|
||||||
|
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the issues URL of this mod!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE mods
|
||||||
|
SET issues_url = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
issues_url.as_deref(),
|
||||||
|
id as database::models::ids::ModId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(source_url) = &new_mod.source_url {
|
||||||
|
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the source URL of this mod!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE mods
|
||||||
|
SET source_url = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
source_url.as_deref(),
|
||||||
|
id as database::models::ids::ModId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(wiki_url) = &new_mod.wiki_url {
|
||||||
|
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the wiki URL of this mod!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE mods
|
||||||
|
SET wiki_url = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
wiki_url.as_deref(),
|
||||||
|
id as database::models::ids::ModId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(body) = &new_mod.body {
|
||||||
|
if !perms.contains(Permissions::EDIT_BODY) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the body of this mod!".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let body_path = format!("data/{}/description.md", mod_id);
|
||||||
|
|
||||||
|
file_host.delete_file_version("", &*body_path).await?;
|
||||||
|
|
||||||
|
file_host
|
||||||
|
.upload_file("text/plain", &body_path, body.clone().into_bytes())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction
|
||||||
|
.commit()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
Ok(HttpResponse::Ok().body(""))
|
||||||
|
} else {
|
||||||
|
Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have permission to edit this mod!".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[delete("{id}")]
|
#[delete("{id}")]
|
||||||
pub async fn mod_delete(
|
pub async fn mod_delete(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
@ -97,7 +462,7 @@ pub async fn mod_delete(
|
|||||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
let id = info.into_inner().0;
|
let id = info.into_inner().0;
|
||||||
|
|
||||||
if user.role != Role::Moderator || user.role != Role::Admin {
|
if !user.role.is_mod() {
|
||||||
let mod_item = database::models::Mod::get(id.into(), &**pool)
|
let mod_item = database::models::Mod::get(id.into(), &**pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||||
@ -122,12 +487,7 @@ pub async fn mod_delete(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
|
delete_from_index(id, config).await?;
|
||||||
|
|
||||||
let indexes: Vec<meilisearch_sdk::indexes::Index> = client.get_indexes().await?;
|
|
||||||
for index in indexes {
|
|
||||||
index.delete_document(format!("local-{}", id)).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
Ok(HttpResponse::Ok().body(""))
|
Ok(HttpResponse::Ok().body(""))
|
||||||
@ -135,3 +495,17 @@ pub async fn mod_delete(
|
|||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_from_index(
|
||||||
|
id: crate::models::mods::ModId,
|
||||||
|
config: web::Data<SearchConfig>,
|
||||||
|
) -> Result<(), meilisearch_sdk::errors::Error> {
|
||||||
|
let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key);
|
||||||
|
|
||||||
|
let indexes: Vec<meilisearch_sdk::indexes::Index> = client.get_indexes().await?;
|
||||||
|
for index in indexes {
|
||||||
|
index.delete_document(format!("local-{}", id)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
use super::ApiError;
|
use super::ApiError;
|
||||||
use crate::auth::get_user_from_headers;
|
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||||
use crate::database;
|
use crate::database;
|
||||||
|
use crate::file_hosting::FileHost;
|
||||||
use crate::models;
|
use crate::models;
|
||||||
use crate::models::teams::Permissions;
|
use crate::models::teams::Permissions;
|
||||||
use crate::models::users::Role;
|
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||||
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
// TODO: this needs filtering, and a better response type
|
// TODO: this needs filtering, and a better response type
|
||||||
// Currently it only gives a list of ids, which have to be
|
// Currently it only gives a list of ids, which have to be
|
||||||
@ -52,6 +53,7 @@ pub struct VersionIds {
|
|||||||
|
|
||||||
#[get("versions")]
|
#[get("versions")]
|
||||||
pub async fn versions_get(
|
pub async fn versions_get(
|
||||||
|
req: HttpRequest,
|
||||||
web::Query(ids): web::Query<VersionIds>,
|
web::Query(ids): web::Query<VersionIds>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
@ -63,17 +65,48 @@ pub async fn versions_get(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
let versions: Vec<models::mods::Version> = versions_data
|
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||||
.into_iter()
|
|
||||||
.filter_map(|v| v)
|
let mut versions = Vec::new();
|
||||||
.map(convert_version)
|
|
||||||
.collect();
|
for version_data in versions_data {
|
||||||
|
if let Some(version) = version_data {
|
||||||
|
let mut authorized = version.accepted;
|
||||||
|
|
||||||
|
if let Some(user) = &user_option {
|
||||||
|
if !authorized {
|
||||||
|
if user.role.is_mod() {
|
||||||
|
authorized = true;
|
||||||
|
} else {
|
||||||
|
let user_id: database::models::ids::UserId = user.id.into();
|
||||||
|
|
||||||
|
let member_exists = sqlx::query!(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM team_members tm INNER JOIN mods m ON m.team_id = tm.id AND m.id = $1 WHERE tm.user_id = $2)",
|
||||||
|
version.mod_id as database::models::ModId,
|
||||||
|
user_id as database::models::ids::UserId,
|
||||||
|
)
|
||||||
|
.fetch_one(&**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||||
|
.exists;
|
||||||
|
|
||||||
|
authorized = member_exists.unwrap_or(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if authorized {
|
||||||
|
versions.push(convert_version(version));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(versions))
|
Ok(HttpResponse::Ok().json(versions))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("{version_id}")]
|
#[get("{version_id}")]
|
||||||
pub async fn version_get(
|
pub async fn version_get(
|
||||||
|
req: HttpRequest,
|
||||||
info: web::Path<(models::ids::VersionId,)>,
|
info: web::Path<(models::ids::VersionId,)>,
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
@ -81,8 +114,29 @@ pub async fn version_get(
|
|||||||
let version_data = database::models::Version::get_full(id.into(), &**pool)
|
let version_data = database::models::Version::get_full(id.into(), &**pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||||
|
|
||||||
if let Some(data) = version_data {
|
if let Some(data) = version_data {
|
||||||
|
if let Some(user) = user_option {
|
||||||
|
if !data.accepted && !user.role.is_mod() {
|
||||||
|
let user_id: database::models::ids::UserId = user.id.into();
|
||||||
|
|
||||||
|
let member_exists = sqlx::query!(
|
||||||
|
"SELECT EXISTS(SELECT 1 FROM team_members tm INNER JOIN mods m ON m.team_id = tm.id AND m.id = $1 WHERE tm.user_id = $2)",
|
||||||
|
data.mod_id as database::models::ModId,
|
||||||
|
user_id as database::models::ids::UserId,
|
||||||
|
)
|
||||||
|
.fetch_one(&**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||||
|
.exists;
|
||||||
|
|
||||||
|
if !member_exists.unwrap_or(false) {
|
||||||
|
return Ok(HttpResponse::NotFound().body(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(convert_version(data)))
|
Ok(HttpResponse::Ok().json(convert_version(data)))
|
||||||
} else {
|
} else {
|
||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
@ -106,7 +160,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
|||||||
"release" => VersionType::Release,
|
"release" => VersionType::Release,
|
||||||
"beta" => VersionType::Beta,
|
"beta" => VersionType::Beta,
|
||||||
"alpha" => VersionType::Alpha,
|
"alpha" => VersionType::Alpha,
|
||||||
_ => VersionType::Alpha,
|
_ => VersionType::Release,
|
||||||
},
|
},
|
||||||
|
|
||||||
files: data
|
files: data
|
||||||
@ -141,6 +195,228 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct EditVersion {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub changelog: Option<String>,
|
||||||
|
pub version_type: Option<models::mods::VersionType>,
|
||||||
|
pub dependencies: Option<Vec<models::ids::VersionId>>,
|
||||||
|
pub game_versions: Option<Vec<models::mods::GameVersion>>,
|
||||||
|
pub loaders: Option<Vec<models::mods::ModLoader>>,
|
||||||
|
pub accepted: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("{id}")]
|
||||||
|
pub async fn version_edit(
|
||||||
|
req: HttpRequest,
|
||||||
|
info: web::Path<(models::ids::VersionId,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||||
|
new_version: web::Json<EditVersion>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
|
|
||||||
|
let version_id = info.into_inner().0;
|
||||||
|
let id = version_id.into();
|
||||||
|
|
||||||
|
let result = database::models::Version::get_full(id, &**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if let Some(version_item) = result {
|
||||||
|
let mod_item = database::models::Mod::get(version_item.mod_id, &**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::InvalidInputError(
|
||||||
|
"Attempted to edit version not attached to mod. How did this happen?"
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let team_member = database::models::TeamMember::get_from_user_id(
|
||||||
|
mod_item.team_id,
|
||||||
|
user.id.into(),
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let permissions;
|
||||||
|
|
||||||
|
if let Some(member) = team_member {
|
||||||
|
permissions = Some(member.permissions)
|
||||||
|
} else if user.role.is_mod() {
|
||||||
|
permissions = Some(Permissions::ALL)
|
||||||
|
} else {
|
||||||
|
permissions = None
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(perms) = permissions {
|
||||||
|
if !perms.contains(Permissions::UPLOAD_VERSION) {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit this version!".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut transaction = pool
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if let Some(accepted) = &new_version.accepted {
|
||||||
|
if !user.role.is_mod() {
|
||||||
|
return Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have the permissions to edit the approval of this version!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE versions
|
||||||
|
SET accepted = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
accepted,
|
||||||
|
id as database::models::ids::VersionId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name) = &new_version.name {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE versions
|
||||||
|
SET name = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
name,
|
||||||
|
id as database::models::ids::VersionId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(version_type) = &new_version.version_type {
|
||||||
|
let channel = database::models::ids::ChannelId::get_id(
|
||||||
|
version_type.as_str(),
|
||||||
|
&mut *transaction,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::InvalidInputError(
|
||||||
|
"No database entry for version type provided.".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE versions
|
||||||
|
SET release_channel = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
channel as database::models::ids::ChannelId,
|
||||||
|
id as database::models::ids::VersionId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dependencies) = &new_version.dependencies {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM dependencies WHERE dependent_id = $1
|
||||||
|
",
|
||||||
|
id as database::models::ids::VersionId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
for dependency in dependencies {
|
||||||
|
let dependency_id: database::models::ids::VersionId = dependency.clone().into();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
INSERT INTO dependencies (dependent_id, dependency_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
",
|
||||||
|
id as database::models::ids::VersionId,
|
||||||
|
dependency_id as database::models::ids::VersionId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(loaders) = &new_version.loaders {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM loaders_versions WHERE version_id = $1
|
||||||
|
",
|
||||||
|
id as database::models::ids::VersionId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
for loader in loaders {
|
||||||
|
let loader_id =
|
||||||
|
database::models::categories::Loader::get_id(&loader.0, &mut *transaction)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::InvalidInputError(
|
||||||
|
"No database entry for loader provided.".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
INSERT INTO loaders_versions (loader_id, version_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
",
|
||||||
|
loader_id as database::models::ids::LoaderId,
|
||||||
|
id as database::models::ids::VersionId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(body) = &new_version.changelog {
|
||||||
|
let mod_id: models::mods::ModId = version_item.mod_id.into();
|
||||||
|
let body_path = format!(
|
||||||
|
"data/{}/versions/{}/changelog.md",
|
||||||
|
mod_id, version_item.version_number
|
||||||
|
);
|
||||||
|
|
||||||
|
file_host.delete_file_version("", &*body_path).await?;
|
||||||
|
|
||||||
|
file_host
|
||||||
|
.upload_file("text/plain", &body_path, body.clone().into_bytes())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction
|
||||||
|
.commit()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
Ok(HttpResponse::Ok().body(""))
|
||||||
|
} else {
|
||||||
|
Err(ApiError::CustomAuthenticationError(
|
||||||
|
"You do not have permission to edit this version!".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[delete("{version_id}")]
|
#[delete("{version_id}")]
|
||||||
pub async fn version_delete(
|
pub async fn version_delete(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
@ -150,7 +426,7 @@ pub async fn version_delete(
|
|||||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||||
let id = info.into_inner().0;
|
let id = info.into_inner().0;
|
||||||
|
|
||||||
if user.role != Role::Moderator || user.role != Role::Admin {
|
if user.role.is_mod() {
|
||||||
let version = database::models::Version::get(id.into(), &**pool)
|
let version = database::models::Version::get(id.into(), &**pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||||
@ -192,3 +468,139 @@ pub async fn version_delete(
|
|||||||
Ok(HttpResponse::NotFound().body(""))
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Algorithm {
|
||||||
|
#[serde(default = "default_algorithm")]
|
||||||
|
algorithm: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_algorithm() -> String {
|
||||||
|
"sha1".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// under /api/v1/version_file/{hash}
|
||||||
|
#[get("{version_id}")]
|
||||||
|
pub async fn get_version_from_hash(
|
||||||
|
info: web::Path<(String,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
algorithm: web::Query<Algorithm>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let hash = info.into_inner().0;
|
||||||
|
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT version_id FROM files
|
||||||
|
INNER JOIN hashes ON hash = $1 AND algorithm = $2
|
||||||
|
",
|
||||||
|
hash.as_bytes(),
|
||||||
|
algorithm.algorithm
|
||||||
|
)
|
||||||
|
.fetch_optional(&**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if let Some(id) = result {
|
||||||
|
let version_data = database::models::Version::get_full(
|
||||||
|
database::models::VersionId(id.version_id),
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if let Some(data) = version_data {
|
||||||
|
Ok(HttpResponse::Ok().json(convert_version(data)))
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// under /api/v1/version_file/{hash}
|
||||||
|
#[delete("{version_id}")]
|
||||||
|
pub async fn delete_file(
|
||||||
|
req: HttpRequest,
|
||||||
|
info: web::Path<(String,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||||
|
algorithm: web::Query<Algorithm>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
check_is_moderator_from_headers(req.headers(), &**pool).await?;
|
||||||
|
|
||||||
|
let hash = info.into_inner().0;
|
||||||
|
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT version_id, filename FROM files
|
||||||
|
INNER JOIN hashes ON hash = $1 AND algorithm = $2
|
||||||
|
",
|
||||||
|
hash.as_bytes(),
|
||||||
|
algorithm.algorithm
|
||||||
|
)
|
||||||
|
.fetch_optional(&**pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if let Some(row) = result {
|
||||||
|
let version_data = database::models::Version::get_full(
|
||||||
|
database::models::VersionId(row.version_id),
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
if let Some(data) = version_data {
|
||||||
|
let mut transaction = pool
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM hashes
|
||||||
|
WHERE hash = $1 AND algorithm = $2
|
||||||
|
",
|
||||||
|
hash.as_bytes(),
|
||||||
|
algorithm.algorithm
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM files
|
||||||
|
WHERE files.version_id = $1
|
||||||
|
",
|
||||||
|
data.id as database::models::ids::VersionId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
let mod_id: models::mods::ModId = data.mod_id.into();
|
||||||
|
file_host
|
||||||
|
.delete_file_version(
|
||||||
|
"",
|
||||||
|
&format!(
|
||||||
|
"data/{}/versions/{}/{}",
|
||||||
|
mod_id, data.version_number, row.filename
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction
|
||||||
|
.commit()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().body(""))
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -14,12 +14,29 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
|||||||
|
|
||||||
let mut mods = sqlx::query!(
|
let mut mods = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id FROM mods m
|
SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status FROM mods m
|
||||||
"
|
"
|
||||||
).fetch(&pool);
|
).fetch(&pool);
|
||||||
|
|
||||||
while let Some(result) = mods.next().await {
|
while let Some(result) = mods.next().await {
|
||||||
if let Ok(mod_data) = result {
|
if let Ok(mod_data) = result {
|
||||||
|
let status = crate::models::mods::ModStatus::from_str(
|
||||||
|
&sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT status FROM statuses
|
||||||
|
WHERE id = $1
|
||||||
|
",
|
||||||
|
mod_data.status,
|
||||||
|
)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await?
|
||||||
|
.status,
|
||||||
|
);
|
||||||
|
|
||||||
|
if !status.is_searchable() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let versions = sqlx::query!(
|
let versions = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT DISTINCT gv.version, gv.created FROM versions
|
SELECT DISTINCT gv.version, gv.created FROM versions
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user