You've already forked AstralRinth
forked from didirus/AstralRinth
More mod info (#104)
* More mod info * Downloading mods * Run prepare * User editing + icon editing * Finish * Some fixes * Fix clippy errors
This commit is contained in:
@@ -26,9 +26,11 @@ pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
||||
|
||||
cfg.service(
|
||||
web::scope("mod")
|
||||
.service(mods::mod_slug_get)
|
||||
.service(mods::mod_get)
|
||||
.service(mods::mod_delete)
|
||||
.service(mods::mod_edit)
|
||||
.service(mods::mod_icon_edit)
|
||||
.service(web::scope("{mod_id}").service(versions::version_list)),
|
||||
);
|
||||
}
|
||||
@@ -46,7 +48,8 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("version_file")
|
||||
.service(versions::delete_file)
|
||||
.service(versions::get_version_from_hash),
|
||||
.service(versions::get_version_from_hash)
|
||||
.service(versions::download_version),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,9 +59,12 @@ pub fn users_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(users::users_get);
|
||||
cfg.service(
|
||||
web::scope("user")
|
||||
.service(users::user_username_get)
|
||||
.service(users::user_get)
|
||||
.service(users::mods_list)
|
||||
.service(users::user_delete)
|
||||
.service(users::user_edit)
|
||||
.service(users::user_icon_edit)
|
||||
.service(users::teams),
|
||||
);
|
||||
}
|
||||
@@ -84,6 +90,8 @@ pub fn moderation_config(cfg: &mut web::ServiceConfig) {
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ApiError {
|
||||
#[error("Environment Error")]
|
||||
EnvError(#[from] dotenv::Error),
|
||||
#[error("Error while uploading file")]
|
||||
FileHostingError(#[from] FileHostingError),
|
||||
#[error("Internal server error")]
|
||||
@@ -103,6 +111,7 @@ pub enum ApiError {
|
||||
impl actix_web::ResponseError for ApiError {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
match self {
|
||||
ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||
ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED,
|
||||
@@ -117,6 +126,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
actix_web::web::HttpResponse::build(self.status_code()).json(
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
ApiError::EnvError(..) => "environment_error",
|
||||
ApiError::DatabaseError(..) => "database_error",
|
||||
ApiError::AuthenticationError(..) => "unauthorized",
|
||||
ApiError::CustomAuthenticationError(..) => "unauthorized",
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::auth::{get_user_from_headers, AuthenticationError};
|
||||
use crate::database::models;
|
||||
use crate::file_hosting::{FileHost, FileHostingError};
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::mods::{ModId, ModStatus, VersionId};
|
||||
use crate::models::mods::{DonationLink, License, ModId, ModStatus, SideType, VersionId};
|
||||
use crate::models::users::UserId;
|
||||
use crate::routes::version_creation::InitialVersionData;
|
||||
use crate::search::indexing::{queue::CreationQueue, IndexingError};
|
||||
@@ -99,6 +99,8 @@ impl actix_web::ResponseError for CreateError {
|
||||
struct ModCreateData {
|
||||
/// The title or name of the mod.
|
||||
pub mod_name: String,
|
||||
/// The slug of a mod, used for vanity URLs
|
||||
pub mod_slug: Option<String>,
|
||||
/// A short description of the mod.
|
||||
pub mod_description: String,
|
||||
/// A long description of the mod, in markdown.
|
||||
@@ -113,8 +115,20 @@ 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 link to the mod's license page
|
||||
pub license_url: Option<String>,
|
||||
/// An optional link to the mod's discord.
|
||||
pub discord_url: Option<String>,
|
||||
/// An optional boolean. If true, the mod will be created as a draft.
|
||||
pub is_draft: Option<bool>,
|
||||
/// The support range for the client mod
|
||||
pub client_side: SideType,
|
||||
/// The support range for the server mod
|
||||
pub server_side: SideType,
|
||||
/// The license id that the mod follows
|
||||
pub license_id: String,
|
||||
/// An optional list of all donation links the mod has
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
}
|
||||
|
||||
pub struct UploadedFile {
|
||||
@@ -461,7 +475,53 @@ async fn mod_create_inner(
|
||||
|
||||
let status_id = models::StatusId::get_id(&status, &mut *transaction)
|
||||
.await?
|
||||
.expect("No database entry found for status");
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!("Status {} does not exist.", status.clone()))
|
||||
})?;
|
||||
let client_side_id =
|
||||
models::SideTypeId::get_id(&mod_create_data.client_side, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
"Client side type specified does not exist.".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let server_side_id =
|
||||
models::SideTypeId::get_id(&mod_create_data.server_side, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(
|
||||
"Server side type specified does not exist.".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let license_id =
|
||||
models::categories::License::get_id(&mod_create_data.license_id, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput("License specified does not exist.".to_string())
|
||||
})?;
|
||||
let mut donation_urls = vec![];
|
||||
|
||||
if let Some(urls) = &mod_create_data.donation_urls {
|
||||
for url in urls {
|
||||
let platform_id = models::DonationPlatformId::get_id(&url.id, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidInput(format!(
|
||||
"Donation platform {} does not exist.",
|
||||
url.id.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
donation_urls.push(models::mod_item::DonationUrl {
|
||||
mod_id: mod_id.into(),
|
||||
platform_id,
|
||||
url: url.url.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let mod_builder = models::mod_item::ModBuilder {
|
||||
mod_id: mod_id.into(),
|
||||
@@ -474,15 +534,23 @@ async fn mod_create_inner(
|
||||
source_url: mod_create_data.source_url,
|
||||
wiki_url: mod_create_data.wiki_url,
|
||||
|
||||
license_url: mod_create_data.license_url,
|
||||
discord_url: mod_create_data.discord_url,
|
||||
categories,
|
||||
initial_versions: versions,
|
||||
status: status_id,
|
||||
client_side: client_side_id,
|
||||
server_side: server_side_id,
|
||||
license: license_id,
|
||||
slug: mod_create_data.mod_slug,
|
||||
donation_urls,
|
||||
};
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let response = crate::models::mods::Mod {
|
||||
id: mod_id,
|
||||
slug: mod_builder.slug.clone(),
|
||||
team: team_id.into(),
|
||||
title: mod_builder.title.clone(),
|
||||
description: mod_builder.description.clone(),
|
||||
@@ -490,6 +558,13 @@ async fn mod_create_inner(
|
||||
published: now,
|
||||
updated: now,
|
||||
status,
|
||||
license: License {
|
||||
id: mod_create_data.license_id.clone(),
|
||||
name: "".to_string(),
|
||||
url: mod_builder.license_url.clone(),
|
||||
},
|
||||
client_side: mod_create_data.client_side,
|
||||
server_side: mod_create_data.server_side,
|
||||
downloads: 0,
|
||||
categories: mod_create_data.categories,
|
||||
versions: mod_builder
|
||||
@@ -501,6 +576,8 @@ async fn mod_create_inner(
|
||||
issues_url: mod_builder.issues_url.clone(),
|
||||
source_url: mod_builder.source_url.clone(),
|
||||
wiki_url: mod_builder.wiki_url.clone(),
|
||||
discord_url: mod_builder.discord_url.clone(),
|
||||
donation_urls: mod_create_data.donation_urls.clone(),
|
||||
};
|
||||
|
||||
let _mod_id = mod_builder.insert(&mut *transaction).await?;
|
||||
@@ -598,6 +675,7 @@ async fn create_initial_version(
|
||||
game_versions,
|
||||
loaders,
|
||||
release_channel,
|
||||
featured: version_data.featured,
|
||||
};
|
||||
|
||||
Ok(version)
|
||||
@@ -642,7 +720,7 @@ async fn process_icon_upload(
|
||||
}
|
||||
}
|
||||
|
||||
fn get_image_content_type(extension: &str) -> Option<&'static str> {
|
||||
pub fn get_image_content_type(extension: &str) -> Option<&'static str> {
|
||||
let content_type = match &*extension {
|
||||
"bmp" => "image/bmp",
|
||||
"gif" => "image/gif",
|
||||
|
||||
@@ -2,10 +2,12 @@ use super::ApiError;
|
||||
use crate::auth::check_is_moderator_from_headers;
|
||||
use crate::database;
|
||||
use crate::models;
|
||||
use crate::models::mods::{ModStatus, VersionType};
|
||||
use crate::models::mods::{ModStatus, VersionType, ModId};
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||
use serde::Deserialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::PgPool;
|
||||
use sqlx::types::chrono::{DateTime, Utc};
|
||||
use crate::models::teams::TeamId;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResultCount {
|
||||
@@ -17,6 +19,42 @@ fn default_count() -> i16 {
|
||||
100
|
||||
}
|
||||
|
||||
/// A mod returned from the API moderation routes
|
||||
#[derive(Serialize)]
|
||||
pub struct ModerationMod {
|
||||
/// The ID of the mod, encoded as a base62 string.
|
||||
pub id: ModId,
|
||||
/// The slug of a mod, used for vanity URLs
|
||||
pub slug: Option<String>,
|
||||
/// The team of people that has ownership of this mod.
|
||||
pub team: TeamId,
|
||||
/// The title or name of the mod.
|
||||
pub title: String,
|
||||
/// A short description of the mod.
|
||||
pub description: String,
|
||||
/// The link to the long description of the mod.
|
||||
pub body_url: String,
|
||||
/// The date at which the mod was first published.
|
||||
pub published: DateTime<Utc>,
|
||||
/// The date at which the mod was first published.
|
||||
pub updated: DateTime<Utc>,
|
||||
/// The status of the mod
|
||||
pub status: ModStatus,
|
||||
|
||||
/// The total number of downloads the mod has had.
|
||||
pub downloads: u32,
|
||||
/// The URL of the icon of the mod
|
||||
pub icon_url: Option<String>,
|
||||
/// An optional link to where to submit bugs or issues with the mod.
|
||||
pub issues_url: Option<String>,
|
||||
/// An optional link to the source code for the mod.
|
||||
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 link to the mod's discord
|
||||
pub discord_url: Option<String>,
|
||||
}
|
||||
|
||||
#[get("mods")]
|
||||
pub async fn mods(
|
||||
req: HttpRequest,
|
||||
@@ -41,15 +79,14 @@ pub async fn mods(
|
||||
)
|
||||
.fetch_many(&**pool)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|m| models::mods::Mod {
|
||||
Ok(e.right().map(|m| ModerationMod {
|
||||
id: database::models::ids::ModId(m.id).into(),
|
||||
slug: m.slug,
|
||||
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,
|
||||
@@ -57,9 +94,10 @@ pub async fn mods(
|
||||
updated: m.updated,
|
||||
downloads: m.downloads as u32,
|
||||
wiki_url: m.wiki_url,
|
||||
discord_url: m.discord_url,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<models::mods::Mod>>()
|
||||
.try_collect::<Vec<ModerationMod>>()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
@@ -92,6 +130,7 @@ pub async fn versions(
|
||||
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(),
|
||||
featured: m.featured,
|
||||
name: m.name,
|
||||
version_number: m.version_number,
|
||||
changelog_url: m.changelog_url,
|
||||
|
||||
@@ -3,10 +3,11 @@ use crate::auth::get_user_from_headers;
|
||||
use crate::database;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models;
|
||||
use crate::models::mods::{ModStatus, SearchRequest};
|
||||
use crate::models::mods::{DonationLink, License, ModStatus, SearchRequest, SideType};
|
||||
use crate::models::teams::Permissions;
|
||||
use crate::search::{search_for_mod, SearchConfig, SearchError};
|
||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
@@ -80,6 +81,53 @@ pub async fn mods_get(
|
||||
Ok(HttpResponse::Ok().json(mods))
|
||||
}
|
||||
|
||||
#[get("@{id}")]
|
||||
pub async fn mod_slug_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let mod_data = database::models::Mod::get_full_from_slug(id, &**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 {
|
||||
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(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{id}")]
|
||||
pub async fn mod_get(
|
||||
req: HttpRequest,
|
||||
@@ -132,6 +180,7 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
||||
|
||||
models::mods::Mod {
|
||||
id: m.id.into(),
|
||||
slug: m.slug,
|
||||
team: m.team_id.into(),
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
@@ -139,6 +188,13 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
||||
published: m.published,
|
||||
updated: m.updated,
|
||||
status: data.status,
|
||||
license: License {
|
||||
id: data.license_id,
|
||||
name: data.license_name,
|
||||
url: m.license_url,
|
||||
},
|
||||
client_side: data.client_side,
|
||||
server_side: data.server_side,
|
||||
downloads: m.downloads as u32,
|
||||
categories: data.categories,
|
||||
versions: data.versions.into_iter().map(|v| v.into()).collect(),
|
||||
@@ -146,6 +202,8 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod
|
||||
issues_url: m.issues_url,
|
||||
source_url: m.source_url,
|
||||
wiki_url: m.wiki_url,
|
||||
discord_url: m.discord_url,
|
||||
donation_urls: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +233,28 @@ pub struct EditMod {
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub wiki_url: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub license_url: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub discord_url: Option<Option<String>>,
|
||||
pub donation_urls: Option<Vec<DonationLink>>,
|
||||
pub license_id: Option<String>,
|
||||
pub client_side: Option<SideType>,
|
||||
pub server_side: Option<SideType>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub slug: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
@@ -270,12 +350,10 @@ pub async fn mod_edit(
|
||||
));
|
||||
}
|
||||
|
||||
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(),
|
||||
));
|
||||
}
|
||||
if (status == &ModStatus::Rejected || status == &ModStatus::Approved) && !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)
|
||||
@@ -421,6 +499,199 @@ pub async fn mod_edit(
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(license_url) = &new_mod.license_url {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the license URL of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET license_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
license_url.as_deref(),
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(discord_url) = &new_mod.discord_url {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the discord URL of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET discord_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
discord_url.as_deref(),
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(slug) = &new_mod.slug {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the slug of this mod!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET slug = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
slug.as_deref(),
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(new_side) = &new_mod.client_side {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the side type of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let side_type_id =
|
||||
database::models::SideTypeId::get_id(new_side, &mut *transaction)
|
||||
.await?
|
||||
.expect("No database entry found for side type");
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET client_side = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
side_type_id as database::models::SideTypeId,
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(new_side) = &new_mod.server_side {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the side type of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let side_type_id =
|
||||
database::models::SideTypeId::get_id(new_side, &mut *transaction)
|
||||
.await?
|
||||
.expect("No database entry found for side type");
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET server_side = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
side_type_id as database::models::SideTypeId,
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(license) = &new_mod.license_id {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the license of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let license_id =
|
||||
database::models::categories::License::get_id(license, &mut *transaction)
|
||||
.await?
|
||||
.expect("No database entry found for license");
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET license = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
license_id as database::models::LicenseId,
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(donations) = &new_mod.donation_urls {
|
||||
if !perms.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the donation links of this mod!"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods_donations
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
for donation in donations {
|
||||
let platform_id = database::models::DonationPlatformId::get_id(
|
||||
&donation.id,
|
||||
&mut *transaction,
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError(format!(
|
||||
"Platform {} does not exist.",
|
||||
donation.id.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url)
|
||||
VALUES ($1, $2, $3)
|
||||
",
|
||||
id as database::models::ids::ModId,
|
||||
platform_id as database::models::ids::DonationPlatformId,
|
||||
donation.url
|
||||
)
|
||||
.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(
|
||||
@@ -452,6 +723,99 @@ pub async fn mod_edit(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Extension {
|
||||
pub ext: String,
|
||||
}
|
||||
|
||||
#[patch("{id}/icon")]
|
||||
pub async fn mod_icon_edit(
|
||||
web::Query(ext): web::Query<Extension>,
|
||||
req: HttpRequest,
|
||||
info: web::Path<(models::ids::ModId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = super::mod_creation::get_image_content_type(&*ext.ext) {
|
||||
let cdn_url = dotenv::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0;
|
||||
|
||||
let mod_item = database::models::Mod::get(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
mod_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::DatabaseError)?
|
||||
.ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?;
|
||||
|
||||
if !team_member.permissions.contains(Permissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to edit this mod's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = mod_item.icon_url {
|
||||
let name = icon.split('/').next();
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut bytes = web::BytesMut::new();
|
||||
while let Some(item) = payload.next().await {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string())
|
||||
})?);
|
||||
}
|
||||
|
||||
if bytes.len() >= 262144 {
|
||||
return Err(ApiError::InvalidInputError(String::from(
|
||||
"Icons must be smaller than 256KiB",
|
||||
)));
|
||||
}
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/icon.{}", id, ext.ext),
|
||||
bytes.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mod_id: database::models::ids::ModId = id.into();
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET icon_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
mod_id as database::models::ids::ModId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInputError(format!(
|
||||
"Invalid format for mod icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("{id}")]
|
||||
pub async fn mod_delete(
|
||||
req: HttpRequest,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::check_is_admin_from_headers;
|
||||
use crate::database::models;
|
||||
use crate::database::models::categories::{DonationPlatform, License};
|
||||
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
|
||||
use models::categories::{Category, GameVersion, Loader};
|
||||
use sqlx::PgPool;
|
||||
@@ -16,7 +17,13 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.service(loader_delete)
|
||||
.service(game_version_list)
|
||||
.service(game_version_create)
|
||||
.service(game_version_delete),
|
||||
.service(game_version_delete)
|
||||
.service(license_create)
|
||||
.service(license_delete)
|
||||
.service(license_list)
|
||||
.service(donation_platform_create)
|
||||
.service(donation_platform_list)
|
||||
.service(donation_platform_delete),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,14 +41,7 @@ pub async fn category_create(
|
||||
pool: web::Data<PgPool>,
|
||||
category: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = category.into_inner().0;
|
||||
|
||||
@@ -56,14 +56,7 @@ pub async fn category_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
category: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = category.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
@@ -94,14 +87,7 @@ pub async fn loader_create(
|
||||
pool: web::Data<PgPool>,
|
||||
loader: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = loader.into_inner().0;
|
||||
|
||||
@@ -116,14 +102,7 @@ pub async fn loader_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
loader: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = loader.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
@@ -176,14 +155,7 @@ pub async fn game_version_create(
|
||||
game_version: web::Path<(String,)>,
|
||||
version_data: web::Json<GameVersionData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = game_version.into_inner().0;
|
||||
|
||||
@@ -209,14 +181,7 @@ pub async fn game_version_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
game_version: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(
|
||||
req.headers(),
|
||||
&mut *pool
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?,
|
||||
)
|
||||
.await?;
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = game_version.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
@@ -234,3 +199,141 @@ pub async fn game_version_delete(
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct LicenseQueryData {
|
||||
short: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("license")]
|
||||
pub async fn license_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results: Vec<LicenseQueryData> = License::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| LicenseQueryData {
|
||||
short: x.short,
|
||||
name: x.name,
|
||||
})
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct LicenseData {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[put("license/{name}")]
|
||||
pub async fn license_create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
license: web::Path<(String,)>,
|
||||
license_data: web::Json<LicenseData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let short = license.into_inner().0;
|
||||
|
||||
let _id = License::builder()
|
||||
.short(&short)?
|
||||
.name(&license_data.name)?
|
||||
.insert(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[delete("license/{name}")]
|
||||
pub async fn license_delete(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
license: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = license.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
|
||||
let result = License::remove(&name, &mut transaction).await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct DonationPlatformQueryData {
|
||||
short: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[get("donation_platform")]
|
||||
pub async fn donation_platform_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results: Vec<DonationPlatformQueryData> = DonationPlatform::list(&**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| DonationPlatformQueryData {
|
||||
short: x.short,
|
||||
name: x.name,
|
||||
})
|
||||
.collect();
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct DonationPlatformData {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[put("donation_platform/{name}")]
|
||||
pub async fn donation_platform_create(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
license: web::Path<(String,)>,
|
||||
license_data: web::Json<DonationPlatformData>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let short = license.into_inner().0;
|
||||
|
||||
let _id = DonationPlatform::builder()
|
||||
.short(&short)?
|
||||
.name(&license_data.name)?
|
||||
.insert(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[delete("donation_platform/{name}")]
|
||||
pub async fn donation_platform_delete(
|
||||
req: HttpRequest,
|
||||
pool: web::Data<PgPool>,
|
||||
loader: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
check_is_admin_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let name = loader.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
|
||||
let result = DonationPlatform::remove(&name, &mut transaction).await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use crate::database::models::{TeamMember, User};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::users::{Role, UserId};
|
||||
use crate::routes::ApiError;
|
||||
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
|
||||
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[get("user")]
|
||||
pub async fn user_auth_get(
|
||||
@@ -44,22 +47,30 @@ pub async fn users_get(
|
||||
|
||||
let users: Vec<crate::models::users::User> = users_data
|
||||
.into_iter()
|
||||
.map(|data| crate::models::users::User {
|
||||
id: data.id.into(),
|
||||
github_id: data.github_id.map(|i| i as u64),
|
||||
username: data.username,
|
||||
name: data.name,
|
||||
email: None,
|
||||
avatar_url: data.avatar_url,
|
||||
bio: data.bio,
|
||||
created: data.created,
|
||||
role: Role::from_string(&*data.role),
|
||||
})
|
||||
.map(convert_user)
|
||||
.collect();
|
||||
|
||||
Ok(HttpResponse::Ok().json(users))
|
||||
}
|
||||
|
||||
#[get("@{id}")]
|
||||
pub async fn user_username_get(
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.into_inner().0;
|
||||
let user_data = User::get_from_username(id, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(data) = user_data {
|
||||
let response = convert_user(data);
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{id}")]
|
||||
pub async fn user_get(
|
||||
info: web::Path<(UserId,)>,
|
||||
@@ -71,23 +82,27 @@ pub async fn user_get(
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(data) = user_data {
|
||||
let response = crate::models::users::User {
|
||||
id: data.id.into(),
|
||||
github_id: data.github_id.map(|i| i as u64),
|
||||
username: data.username,
|
||||
name: data.name,
|
||||
email: None,
|
||||
avatar_url: data.avatar_url,
|
||||
bio: data.bio,
|
||||
created: data.created,
|
||||
role: Role::from_string(&*data.role),
|
||||
};
|
||||
let response = convert_user(data);
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_user(data: crate::database::models::user_item::User) -> crate::models::users::User {
|
||||
crate::models::users::User {
|
||||
id: data.id.into(),
|
||||
github_id: data.github_id.map(|i| i as u64),
|
||||
username: data.username,
|
||||
name: data.name,
|
||||
email: None,
|
||||
avatar_url: data.avatar_url,
|
||||
bio: data.bio,
|
||||
created: data.created,
|
||||
role: Role::from_string(&*data.role),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{user_id}/mods")]
|
||||
pub async fn mods_list(
|
||||
info: web::Path<(UserId,)>,
|
||||
@@ -161,6 +176,236 @@ pub async fn teams(
|
||||
Ok(HttpResponse::Ok().json(team_members))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditUser {
|
||||
pub username: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub name: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub email: Option<Option<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub bio: Option<Option<String>>,
|
||||
pub role: Option<String>,
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
pub async fn user_edit(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
new_user: web::Json<EditUser>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
|
||||
let user_id = info.into_inner().0;
|
||||
let id: crate::database::models::ids::UserId = user_id.into();
|
||||
|
||||
if user.id == user_id || user.role.is_mod() {
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(username) = &new_user.username {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET username = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
username,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(name) = &new_user.name {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET name = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
name.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(bio) = &new_user.bio {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET bio = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
bio.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(email) = &new_user.email {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET email = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
email.as_deref(),
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(role) = &new_user.role {
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You do not have the permissions to edit the role of this user!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let role = Role::from_string(role).to_string();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET role = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
role,
|
||||
id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
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 user!".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Extension {
|
||||
pub ext: String,
|
||||
}
|
||||
|
||||
#[patch("{id}/icon")]
|
||||
pub async fn user_icon_edit(
|
||||
web::Query(ext): web::Query<Extension>,
|
||||
req: HttpRequest,
|
||||
info: web::Path<(UserId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = super::mod_creation::get_image_content_type(&*ext.ext) {
|
||||
let cdn_url = dotenv::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(req.headers(), &**pool).await?;
|
||||
let id = info.into_inner().0;
|
||||
|
||||
if user.id != id && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthenticationError(
|
||||
"You don't have permission to edit this user's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut icon_url = user.avatar_url;
|
||||
|
||||
if user.id != id {
|
||||
let new_user = User::get(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(new) = new_user {
|
||||
icon_url = new.avatar_url;
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = icon_url {
|
||||
if icon.starts_with(&cdn_url) {
|
||||
let name = icon.split('/').next();
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut bytes = web::BytesMut::new();
|
||||
while let Some(item) = payload.next().await {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string())
|
||||
})?);
|
||||
}
|
||||
|
||||
if bytes.len() >= 262144 {
|
||||
return Err(ApiError::InvalidInputError(String::from(
|
||||
"Icons must be smaller than 256KiB",
|
||||
)));
|
||||
}
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("user/{}/icon.{}", id, ext.ext),
|
||||
bytes.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mod_id: crate::database::models::ids::UserId = id.into();
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET avatar_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
mod_id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInputError(format!(
|
||||
"Invalid format for user icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this actually do stuff
|
||||
#[delete("{id}")]
|
||||
pub async fn user_delete(
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct InitialVersionData {
|
||||
pub game_versions: Vec<GameVersion>,
|
||||
pub release_channel: VersionType,
|
||||
pub loaders: Vec<ModLoader>,
|
||||
pub featured: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -265,6 +266,7 @@ async fn version_create_inner(
|
||||
game_versions,
|
||||
loaders,
|
||||
release_channel,
|
||||
featured: version_create_data.featured,
|
||||
});
|
||||
|
||||
continue;
|
||||
@@ -298,6 +300,7 @@ async fn version_create_inner(
|
||||
id: builder.version_id.into(),
|
||||
mod_id: builder.mod_id.into(),
|
||||
author_id: user.id,
|
||||
featured: builder.featured,
|
||||
name: builder.name.clone(),
|
||||
version_number: builder.version_number.clone(),
|
||||
changelog_url: builder.changelog_url.clone(),
|
||||
@@ -324,6 +327,7 @@ async fn version_create_inner(
|
||||
.collect(),
|
||||
url: file.url.clone(),
|
||||
filename: file.filename.clone(),
|
||||
primary: file.primary,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
dependencies: version_data.dependencies,
|
||||
@@ -528,6 +532,7 @@ pub async fn upload_file(
|
||||
// bytes, but this is the string version.
|
||||
hash: upload_data.content_sha1.into_bytes(),
|
||||
}],
|
||||
primary: uploaded_files.len() == 1,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::ApiError;
|
||||
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
use crate::database;
|
||||
use crate::{database, Pepper};
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models;
|
||||
use crate::models::teams::Permissions;
|
||||
@@ -151,6 +151,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
||||
mod_id: data.mod_id.into(),
|
||||
author_id: data.author_id.into(),
|
||||
|
||||
featured: data.featured,
|
||||
name: data.name,
|
||||
version_number: data.version_number,
|
||||
changelog_url: data.changelog_url,
|
||||
@@ -178,6 +179,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models
|
||||
.map(|(k, v)| Some((k, String::from_utf8(v).ok()?)))
|
||||
.collect::<Option<_>>()
|
||||
.unwrap_or_else(Default::default),
|
||||
primary: f.primary,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@@ -204,6 +206,8 @@ pub struct EditVersion {
|
||||
pub game_versions: Option<Vec<models::mods::GameVersion>>,
|
||||
pub loaders: Option<Vec<models::mods::ModLoader>>,
|
||||
pub accepted: Option<bool>,
|
||||
pub featured: Option<bool>,
|
||||
pub primary_file: Option<(String, String)>,
|
||||
}
|
||||
|
||||
#[patch("{id}")]
|
||||
@@ -388,6 +392,65 @@ pub async fn version_edit(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(featured) = &new_version.featured {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
SET featured = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
featured,
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
|
||||
if let Some(primary_file) = &new_version.primary_file {
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM files
|
||||
INNER JOIN hashes ON hash = $1 AND algorithm = $2
|
||||
",
|
||||
primary_file.1.as_bytes(),
|
||||
primary_file.0
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInputError(format!(
|
||||
"Specified file with hash {} does not exist.",
|
||||
primary_file.1.clone()
|
||||
))
|
||||
})?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE files
|
||||
SET is_primary = FALSE
|
||||
WHERE (version_id = $1)
|
||||
",
|
||||
id as database::models::ids::VersionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE files
|
||||
SET is_primary = TRUE
|
||||
WHERE (id = $1)
|
||||
",
|
||||
result.id,
|
||||
)
|
||||
.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!(
|
||||
@@ -518,6 +581,102 @@ pub async fn get_version_from_hash(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DownloadRedirect {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}/download
|
||||
#[get("{version_id}/download")]
|
||||
pub async fn download_version(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
algorithm: web::Query<Algorithm>,
|
||||
pepper: web::Data<Pepper>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let hash = info.into_inner().0;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT f.url url, f.id id, f.version_id version_id, v.mod_id mod_id FROM files f
|
||||
INNER JOIN versions v ON v.id = f.version_id
|
||||
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 real_ip = req.connection_info();
|
||||
let ip_option = real_ip.realip_remote_addr();
|
||||
|
||||
if let Some(ip) = ip_option {
|
||||
let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest();
|
||||
|
||||
let download_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)",
|
||||
id.version_id,
|
||||
hash,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists.unwrap_or(false);
|
||||
|
||||
if !download_exists {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO downloads (
|
||||
version_id, identifier
|
||||
)
|
||||
VALUES (
|
||||
$1, $2
|
||||
)
|
||||
",
|
||||
id.version_id,
|
||||
hash
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE versions
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
id.version_id,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET downloads = downloads + 1
|
||||
WHERE id = $1
|
||||
",
|
||||
id.mod_id,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
}
|
||||
}
|
||||
Ok(HttpResponse::TemporaryRedirect()
|
||||
.header("Location", &*id.url)
|
||||
.json(DownloadRedirect { url: id.url }))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
// under /api/v1/version_file/{hash}
|
||||
#[delete("{version_id}")]
|
||||
pub async fn delete_file(
|
||||
|
||||
Reference in New Issue
Block a user