forked from didirus/AstralRinth
move to monorepo dir
This commit is contained in:
@@ -0,0 +1,541 @@
|
||||
use crate::auth::checks::is_visible_collection;
|
||||
use crate::auth::{filter_visible_collections, get_user_from_headers};
|
||||
use crate::database::models::{collection_item, generate_collection_id, project_item};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::collections::{Collection, CollectionStatus};
|
||||
use crate::models::ids::base62_impl::parse_base62;
|
||||
use crate::models::ids::{CollectionId, ProjectId};
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::{database, models};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use chrono::Utc;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route("collections", web::get().to(collections_get));
|
||||
cfg.route("collection", web::post().to(collection_create));
|
||||
|
||||
cfg.service(
|
||||
web::scope("collection")
|
||||
.route("{id}", web::get().to(collection_get))
|
||||
.route("{id}", web::delete().to(collection_delete))
|
||||
.route("{id}", web::patch().to(collection_edit))
|
||||
.route("{id}/icon", web::patch().to(collection_icon_edit))
|
||||
.route("{id}/icon", web::delete().to(delete_collection_icon)),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Clone)]
|
||||
pub struct CollectionCreateData {
|
||||
#[validate(
|
||||
length(min = 3, max = 64),
|
||||
custom(function = "crate::util::validate::validate_name")
|
||||
)]
|
||||
/// The title or name of the project.
|
||||
pub name: String,
|
||||
#[validate(length(min = 3, max = 255))]
|
||||
/// A short description of the collection.
|
||||
pub description: Option<String>,
|
||||
#[validate(length(max = 32))]
|
||||
#[serde(default = "Vec::new")]
|
||||
/// A list of initial projects to use with the created collection
|
||||
pub projects: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn collection_create(
|
||||
req: HttpRequest,
|
||||
collection_create_data: web::Json<CollectionCreateData>,
|
||||
client: Data<PgPool>,
|
||||
redis: Data<RedisPool>,
|
||||
session_queue: Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, CreateError> {
|
||||
let collection_create_data = collection_create_data.into_inner();
|
||||
|
||||
// The currently logged in user
|
||||
let current_user = get_user_from_headers(
|
||||
&req,
|
||||
&**client,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_CREATE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
collection_create_data
|
||||
.validate()
|
||||
.map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let mut transaction = client.begin().await?;
|
||||
|
||||
let collection_id: CollectionId = generate_collection_id(&mut transaction).await?.into();
|
||||
|
||||
let initial_project_ids = project_item::Project::get_many(
|
||||
&collection_create_data.projects,
|
||||
&mut *transaction,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.inner.id.into())
|
||||
.collect::<Vec<ProjectId>>();
|
||||
|
||||
let collection_builder_actual = collection_item::CollectionBuilder {
|
||||
collection_id: collection_id.into(),
|
||||
user_id: current_user.id.into(),
|
||||
name: collection_create_data.name,
|
||||
description: collection_create_data.description,
|
||||
status: CollectionStatus::Listed,
|
||||
projects: initial_project_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|x| x.into())
|
||||
.collect(),
|
||||
};
|
||||
let collection_builder = collection_builder_actual.clone();
|
||||
|
||||
let now = Utc::now();
|
||||
collection_builder_actual.insert(&mut transaction).await?;
|
||||
|
||||
let response = crate::models::collections::Collection {
|
||||
id: collection_id,
|
||||
user: collection_builder.user_id.into(),
|
||||
name: collection_builder.name.clone(),
|
||||
description: collection_builder.description.clone(),
|
||||
created: now,
|
||||
updated: now,
|
||||
icon_url: None,
|
||||
color: None,
|
||||
status: collection_builder.status,
|
||||
projects: initial_project_ids,
|
||||
};
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CollectionIds {
|
||||
pub ids: String,
|
||||
}
|
||||
pub async fn collections_get(
|
||||
req: HttpRequest,
|
||||
web::Query(ids): web::Query<CollectionIds>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let ids = serde_json::from_str::<Vec<&str>>(&ids.ids)?;
|
||||
let ids = ids
|
||||
.into_iter()
|
||||
.map(|x| parse_base62(x).map(|x| database::models::CollectionId(x as i64)))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let collections_data = database::models::Collection::get_many(&ids, &**pool, &redis).await?;
|
||||
|
||||
let user_option = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_READ]),
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
let collections = filter_visible_collections(collections_data, &user_option).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(collections))
|
||||
}
|
||||
|
||||
pub async fn collection_get(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let collection_data = database::models::Collection::get(id, &**pool, &redis).await?;
|
||||
let user_option = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_READ]),
|
||||
)
|
||||
.await
|
||||
.map(|x| x.1)
|
||||
.ok();
|
||||
|
||||
if let Some(data) = collection_data {
|
||||
if is_visible_collection(&data, &user_option).await? {
|
||||
return Ok(HttpResponse::Ok().json(Collection::from(data)));
|
||||
}
|
||||
}
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate)]
|
||||
pub struct EditCollection {
|
||||
#[validate(
|
||||
length(min = 3, max = 64),
|
||||
custom(function = "crate::util::validate::validate_name")
|
||||
)]
|
||||
pub name: Option<String>,
|
||||
#[validate(length(min = 3, max = 256))]
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "::serde_with::rust::double_option"
|
||||
)]
|
||||
pub description: Option<Option<String>>,
|
||||
pub status: Option<CollectionStatus>,
|
||||
#[validate(length(max = 1024))]
|
||||
pub new_projects: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub async fn collection_edit(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
new_collection: web::Json<EditCollection>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
new_collection
|
||||
.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let result = database::models::Collection::get(id, &**pool, &redis).await?;
|
||||
|
||||
if let Some(collection_item) = result {
|
||||
if !can_modify_collection(&collection_item, &user) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
let id = collection_item.id;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
if let Some(name) = &new_collection.name {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET name = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
name.trim(),
|
||||
id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(description) = &new_collection.description {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET description = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
description.as_ref(),
|
||||
id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(status) = &new_collection.status {
|
||||
if !(user.role.is_mod()
|
||||
|| collection_item.status.is_approved() && status.can_be_requested())
|
||||
{
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to set this status!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET status = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
status.to_string(),
|
||||
id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(new_project_ids) = &new_collection.new_projects {
|
||||
// Delete all existing projects
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM collections_mods
|
||||
WHERE collection_id = $1
|
||||
",
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let collection_item_ids = new_project_ids
|
||||
.iter()
|
||||
.map(|_| collection_item.id.0)
|
||||
.collect_vec();
|
||||
let mut validated_project_ids = Vec::new();
|
||||
for project_id in new_project_ids {
|
||||
let project = database::models::Project::get(project_id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"The specified project {project_id} does not exist!"
|
||||
))
|
||||
})?;
|
||||
validated_project_ids.push(project.inner.id.0);
|
||||
}
|
||||
// Insert- don't throw an error if it already exists
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO collections_mods (collection_id, mod_id)
|
||||
SELECT * FROM UNNEST ($1::int8[], $2::int8[])
|
||||
ON CONFLICT DO NOTHING
|
||||
",
|
||||
&collection_item_ids[..],
|
||||
&validated_project_ids[..],
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET updated = NOW()
|
||||
WHERE id = $1
|
||||
",
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Extension {
|
||||
pub ext: String,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn collection_icon_edit(
|
||||
web::Query(ext): web::Query<Extension>,
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let collection_item = database::models::Collection::get(id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified collection does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !can_modify_collection(&collection_item, &user) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
collection_item.icon_url,
|
||||
collection_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let collection_id: CollectionId = collection_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", collection_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET icon_url = $1, raw_icon_url = $2, color = $3
|
||||
WHERE (id = $4)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color.map(|x| x as i32),
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
pub async fn delete_collection_icon(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let collection_item = database::models::Collection::get(id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified collection does not exist!".to_string())
|
||||
})?;
|
||||
if !can_modify_collection(&collection_item, &user) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
collection_item.icon_url,
|
||||
collection_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
pub async fn collection_delete(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_DELETE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let collection = database::models::Collection::get(id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified collection does not exist!".to_string())
|
||||
})?;
|
||||
if !can_modify_collection(&collection, &user) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let result =
|
||||
database::models::Collection::remove(collection.id, &mut transaction, &redis).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Collection::clear_cache(collection.id, &redis).await?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
fn can_modify_collection(
|
||||
collection: &database::models::Collection,
|
||||
user: &models::users::User,
|
||||
) -> bool {
|
||||
collection.user_id == user.id.into() || user.role.is_mod()
|
||||
}
|
||||
Reference in New Issue
Block a user