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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "dashmap"
|
||||
version = "3.11.10"
|
||||
@ -1509,6 +1544,12 @@ dependencies = [
|
||||
"tokio-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.0"
|
||||
@ -1627,6 +1668,7 @@ dependencies = [
|
||||
"rust-s3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"sha1",
|
||||
"sqlx",
|
||||
"sqlx-macros",
|
||||
@ -2397,6 +2439,28 @@ dependencies = [
|
||||
"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]]
|
||||
name = "sha-1"
|
||||
version = "0.9.1"
|
||||
@ -2635,6 +2699,12 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.3.0"
|
||||
|
||||
@ -23,6 +23,7 @@ reqwest = { version = "0.10.8", features = ["json"] }
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_with = "1.5.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
rand = "0.7.3"
|
||||
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?;
|
||||
|
||||
match user.role {
|
||||
Role::Moderator | Role::Admin => Ok(user),
|
||||
_ => Err(AuthenticationError::InvalidCredentialsError),
|
||||
if user.role.is_mod() {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(AuthenticationError::InvalidCredentialsError)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -80,6 +80,7 @@ impl VersionBuilder {
|
||||
date_published: chrono::Utc::now(),
|
||||
downloads: 0,
|
||||
release_channel: self.release_channel,
|
||||
accepted: false,
|
||||
};
|
||||
|
||||
version.insert(&mut *transaction).await?;
|
||||
@ -152,6 +153,7 @@ pub struct Version {
|
||||
pub date_published: chrono::DateTime<chrono::Utc>,
|
||||
pub downloads: i32,
|
||||
pub release_channel: ChannelId,
|
||||
pub accepted: bool,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
@ -164,12 +166,13 @@ impl Version {
|
||||
INSERT INTO versions (
|
||||
id, mod_id, author_id, name, version_number,
|
||||
changelog_url, date_published,
|
||||
downloads, release_channel
|
||||
downloads, release_channel, accepted
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7,
|
||||
$8, $9
|
||||
$8, $9,
|
||||
$10
|
||||
)
|
||||
",
|
||||
self.id as VersionId,
|
||||
@ -181,6 +184,7 @@ impl Version {
|
||||
self.date_published,
|
||||
self.downloads,
|
||||
self.release_channel as ChannelId,
|
||||
self.accepted
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@ -291,6 +295,15 @@ impl Version {
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM dependencies WHERE dependent_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
@ -354,7 +367,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
v.release_channel
|
||||
v.release_channel, v.accepted
|
||||
FROM versions v
|
||||
WHERE v.id = $1
|
||||
",
|
||||
@ -374,6 +387,7 @@ impl Version {
|
||||
date_published: row.date_published,
|
||||
downloads: row.downloads,
|
||||
release_channel: ChannelId(row.release_channel),
|
||||
accepted: row.accepted,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
@ -394,7 +408,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
v.release_channel
|
||||
v.release_channel, accepted
|
||||
FROM versions v
|
||||
WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))
|
||||
",
|
||||
@ -412,6 +426,7 @@ impl Version {
|
||||
date_published: v.date_published,
|
||||
downloads: v.downloads,
|
||||
release_channel: ChannelId(v.release_channel),
|
||||
accepted: v.accepted,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<Version>>()
|
||||
@ -431,7 +446,7 @@ impl Version {
|
||||
"
|
||||
SELECT v.mod_id, v.author_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
release_channels.channel
|
||||
release_channels.channel, v.accepted
|
||||
FROM versions v
|
||||
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
||||
WHERE v.id = $1
|
||||
@ -519,6 +534,7 @@ impl Version {
|
||||
files,
|
||||
loaders,
|
||||
game_versions,
|
||||
accepted: row.accepted,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
@ -570,6 +586,7 @@ pub struct QueryVersion {
|
||||
pub files: Vec<QueryFile>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
pub accepted: bool,
|
||||
}
|
||||
|
||||
pub struct QueryFile {
|
||||
|
||||
@ -58,7 +58,7 @@ pub struct Mod {
|
||||
/// Draft - Mod is not displayed on search, and not 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)
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ModStatus {
|
||||
Approved,
|
||||
@ -103,6 +103,24 @@ impl ModStatus {
|
||||
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
|
||||
|
||||
@ -45,4 +45,11 @@ impl Role {
|
||||
_ => 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 index;
|
||||
mod mod_creation;
|
||||
mod moderation;
|
||||
mod mods;
|
||||
mod not_found;
|
||||
mod tags;
|
||||
@ -16,6 +17,7 @@ pub use tags::config as tags_config;
|
||||
|
||||
pub use self::index::index_get;
|
||||
pub use self::not_found::not_found;
|
||||
use crate::file_hosting::FileHostingError;
|
||||
|
||||
pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(mods::mod_search);
|
||||
@ -26,6 +28,7 @@ pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
||||
web::scope("mod")
|
||||
.service(mods::mod_get)
|
||||
.service(mods::mod_delete)
|
||||
.service(mods::mod_edit)
|
||||
.service(web::scope("{mod_id}").service(versions::version_list)),
|
||||
);
|
||||
}
|
||||
@ -37,7 +40,13 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
||||
web::scope("version")
|
||||
.service(versions::version_get)
|
||||
.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)]
|
||||
pub enum ApiError {
|
||||
#[error("Error while uploading file")]
|
||||
FileHostingError(#[from] FileHostingError),
|
||||
#[error("Internal server error")]
|
||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||
#[error("Deserialization error: {0}")]
|
||||
@ -89,6 +100,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||
ApiError::JsonError(..) => actix_web::http::StatusCode::BAD_REQUEST,
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -102,6 +114,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
||||
ApiError::JsonError(..) => "json_error",
|
||||
ApiError::SearchError(..) => "search_error",
|
||||
ApiError::FileHostingError(..) => "file_hosting_error",
|
||||
ApiError::InvalidInputError(..) => "invalid_input",
|
||||
},
|
||||
description: &self.to_string(),
|
||||
|
||||
@ -113,6 +113,8 @@ struct ModCreateData {
|
||||
pub source_url: Option<String>,
|
||||
/// An optional link to the mod's wiki page or other relevant information.
|
||||
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 {
|
||||
@ -277,6 +279,12 @@ async fn mod_create_inner(
|
||||
check_length(3..=2048, "mod description", &create_data.mod_description)?;
|
||||
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
|
||||
.categories
|
||||
.iter()
|
||||
@ -444,7 +452,13 @@ async fn mod_create_inner(
|
||||
|
||||
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)
|
||||
.await?
|
||||
.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 crate::auth::get_user_from_headers;
|
||||
use crate::database;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models;
|
||||
use crate::models::mods::SearchRequest;
|
||||
use crate::models::mods::{ModStatus, SearchRequest};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::models::users::Role;
|
||||
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 sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[get("mod")]
|
||||
pub async fn mod_search(
|
||||
@ -27,6 +28,7 @@ pub struct ModIds {
|
||||
// TODO: Make this return the full mod struct
|
||||
#[get("mods")]
|
||||
pub async fn mods_get(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<ModIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@ -39,17 +41,48 @@ pub async fn mods_get(
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
let mods = mods_data
|
||||
.into_iter()
|
||||
.filter_map(|m| m)
|
||||
.map(convert_mod)
|
||||
.collect::<Vec<_>>();
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let mut mods = Vec::new();
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
#[get("{id}")]
|
||||
pub async fn mod_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(models::ids::ModId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@ -57,9 +90,38 @@ pub async fn mod_get(
|
||||
let mod_data = database::models::Mod::get_full(id.into(), &**pool)
|
||||
.await
|
||||
.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 {
|
||||
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 {
|
||||
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}")]
|
||||
pub async fn mod_delete(
|
||||
req: HttpRequest,
|
||||
@ -97,7 +462,7 @@ pub async fn mod_delete(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
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)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
@ -122,12 +487,7 @@ pub async fn mod_delete(
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
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?;
|
||||
}
|
||||
delete_from_index(id, config).await?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
@ -135,3 +495,17 @@ pub async fn mod_delete(
|
||||
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 crate::auth::get_user_from_headers;
|
||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use crate::database;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models;
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::models::users::Role;
|
||||
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
|
||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
// TODO: this needs filtering, and a better response type
|
||||
// Currently it only gives a list of ids, which have to be
|
||||
@ -52,6 +53,7 @@ pub struct VersionIds {
|
||||
|
||||
#[get("versions")]
|
||||
pub async fn versions_get(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<VersionIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@ -63,17 +65,48 @@ pub async fn versions_get(
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
let versions: Vec<models::mods::Version> = versions_data
|
||||
.into_iter()
|
||||
.filter_map(|v| v)
|
||||
.map(convert_version)
|
||||
.collect();
|
||||
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
|
||||
|
||||
let mut versions = Vec::new();
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
#[get("{version_id}")]
|
||||
pub async fn version_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(models::ids::VersionId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
@ -81,8 +114,29 @@ pub async fn version_get(
|
||||
let version_data = database::models::Version::get_full(id.into(), &**pool)
|
||||
.await
|
||||
.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(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)))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
@ -106,7 +160,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
||||
"release" => VersionType::Release,
|
||||
"beta" => VersionType::Beta,
|
||||
"alpha" => VersionType::Alpha,
|
||||
_ => VersionType::Alpha,
|
||||
_ => VersionType::Release,
|
||||
},
|
||||
|
||||
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}")]
|
||||
pub async fn version_delete(
|
||||
req: HttpRequest,
|
||||
@ -150,7 +426,7 @@ pub async fn version_delete(
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
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)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
@ -192,3 +468,139 @@ pub async fn version_delete(
|
||||
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!(
|
||||
"
|
||||
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);
|
||||
|
||||
while let Some(result) = mods.next().await {
|
||||
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!(
|
||||
"
|
||||
SELECT DISTINCT gv.version, gv.created FROM versions
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user