diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..fa5c2e0 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,4 @@ +edition = "2021" +imports_layout = "HorizontalVertical" +imports_granularity = "Crate" +group_imports = "One" diff --git a/.woodpecker.yml b/.woodpecker.yml index 9877b28..b3568f9 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -4,13 +4,14 @@ variables: steps: cargo_fmt: - image: *rust_image + image: rustlang/rust:nightly environment: # store cargo data in repo folder so that it gets cached between steps CARGO_HOME: .cargo_home commands: - rustup component add rustfmt - - cargo fmt -- --check + - cargo +nightly fmt -- --check + check_config_defaults_updated: image: *rust_image diff --git a/migrations/2023-11-28-150402_ibis_setup/up.sql b/migrations/2023-11-28-150402_ibis_setup/up.sql index 4a022fd..86d297d 100644 --- a/migrations/2023-11-28-150402_ibis_setup/up.sql +++ b/migrations/2023-11-28-150402_ibis_setup/up.sql @@ -43,7 +43,8 @@ create table article ( text text not null, ap_id varchar(255) not null unique, instance_id int REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, - local bool not null + local bool not null, + protected bool not null ); create table edit ( diff --git a/src/backend/api/article.rs b/src/backend/api/article.rs index 421d4d9..592ecce 100644 --- a/src/backend/api/article.rs +++ b/src/backend/api/article.rs @@ -1,27 +1,38 @@ -use crate::backend::database::article::DbArticleForm; -use crate::backend::database::conflict::{DbConflict, DbConflictForm}; -use crate::backend::database::edit::DbEditForm; -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::backend::federation::activities::create_article::CreateArticle; -use crate::backend::federation::activities::submit_article_update; -use crate::backend::utils::generate_article_version; -use crate::common::utils::extract_domain; -use crate::common::utils::http_protocol_str; -use crate::common::validation::can_edit_article; -use crate::common::LocalUserView; -use crate::common::{ApiConflict, ResolveObject}; -use crate::common::{ArticleView, DbArticle, DbEdit}; -use crate::common::{CreateArticleData, EditArticleData, EditVersion, ForkArticleData}; -use crate::common::{DbInstance, SearchArticleData}; -use crate::common::{GetArticleData, ListArticlesData}; -use activitypub_federation::config::Data; -use activitypub_federation::fetch::object_id::ObjectId; +use crate::{ + backend::{ + database::{ + article::DbArticleForm, + conflict::{DbConflict, DbConflictForm}, + edit::DbEditForm, + IbisData, + }, + error::MyResult, + federation::activities::{create_article::CreateArticle, submit_article_update}, + utils::generate_article_version, + }, + common::{ + utils::{extract_domain, http_protocol_str}, + validation::can_edit_article, + ApiConflict, + ArticleView, + CreateArticleForm, + DbArticle, + DbEdit, + DbInstance, + EditArticleForm, + EditVersion, + ForkArticleForm, + GetArticleForm, + ListArticlesForm, + LocalUserView, + ProtectArticleForm, + ResolveObject, + SearchArticleForm, + }, +}; +use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; use anyhow::anyhow; -use axum::extract::Query; -use axum::Extension; -use axum::Form; -use axum::Json; +use axum::{extract::Query, Extension, Form, Json}; use axum_macros::debug_handler; use chrono::Utc; use diffy::create_patch; @@ -31,7 +42,7 @@ use diffy::create_patch; pub(in crate::backend::api) async fn create_article( Extension(user): Extension, data: Data, - Form(create_article): Form, + Form(create_article): Form, ) -> MyResult> { if create_article.title.is_empty() { return Err(anyhow!("Title must not be empty").into()); @@ -50,10 +61,11 @@ pub(in crate::backend::api) async fn create_article( ap_id, instance_id: local_instance.id, local: true, + protected: false, }; let article = DbArticle::create(form, &data)?; - let edit_data = EditArticleData { + let edit_data = EditArticleForm { article_id: article.id, new_text: create_article.text, summary: create_article.summary, @@ -81,7 +93,7 @@ pub(in crate::backend::api) async fn create_article( pub(in crate::backend::api) async fn edit_article( Extension(user): Extension, data: Data, - Form(mut edit_form): Form, + Form(mut edit_form): Form, ) -> MyResult>> { // resolve conflict if any if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id { @@ -136,7 +148,7 @@ pub(in crate::backend::api) async fn edit_article( /// Retrieve an article by ID. It must already be stored in the local database. #[debug_handler] pub(in crate::backend::api) async fn get_article( - Query(query): Query, + Query(query): Query, data: Data, ) -> MyResult> { match (query.title, query.id) { @@ -157,7 +169,7 @@ pub(in crate::backend::api) async fn get_article( #[debug_handler] pub(in crate::backend::api) async fn list_articles( - Query(query): Query, + Query(query): Query, data: Data, ) -> MyResult>> { let only_local = query.only_local.unwrap_or(false); @@ -170,7 +182,7 @@ pub(in crate::backend::api) async fn list_articles( pub(in crate::backend::api) async fn fork_article( Extension(_user): Extension, data: Data, - Form(fork_form): Form, + Form(fork_form): Form, ) -> MyResult> { // TODO: lots of code duplicated from create_article(), can move it into helper let original_article = DbArticle::read(fork_form.article_id, &data)?; @@ -188,6 +200,7 @@ pub(in crate::backend::api) async fn fork_article( ap_id, instance_id: local_instance.id, local: true, + protected: false, }; let article = DbArticle::create(form, &data)?; @@ -242,7 +255,7 @@ pub(super) async fn resolve_article( /// Search articles for matching title or body text. #[debug_handler] pub(super) async fn search_article( - Query(query): Query, + Query(query): Query, data: Data, ) -> MyResult>> { if query.query.is_empty() { @@ -251,3 +264,17 @@ pub(super) async fn search_article( let article = DbArticle::search(&query.query, &data)?; Ok(Json(article)) } + +#[debug_handler] +pub(in crate::backend::api) async fn protect_article( + Extension(user): Extension, + data: Data, + Form(lock_params): Form, +) -> MyResult> { + if !user.local_user.admin { + return Err(anyhow!("Only admin can lock articles").into()); + } + let article = + DbArticle::update_protected(lock_params.article_id, lock_params.protected, &data)?; + Ok(Json(article)) +} diff --git a/src/backend/api/instance.rs b/src/backend/api/instance.rs index 4f133c1..ac21975 100644 --- a/src/backend/api/instance.rs +++ b/src/backend/api/instance.rs @@ -1,13 +1,9 @@ -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::backend::federation::activities::follow::Follow; -use crate::common::{DbInstance, InstanceView, ResolveObject}; -use crate::common::{FollowInstance, LocalUserView}; -use activitypub_federation::config::Data; -use activitypub_federation::fetch::object_id::ObjectId; -use axum::extract::Query; -use axum::Extension; -use axum::{Form, Json}; +use crate::{ + backend::{database::IbisData, error::MyResult, federation::activities::follow::Follow}, + common::{DbInstance, FollowInstance, InstanceView, LocalUserView, ResolveObject}, +}; +use activitypub_federation::{config::Data, fetch::object_id::ObjectId}; +use axum::{extract::Query, Extension, Form, Json}; use axum_macros::debug_handler; /// Retrieve the local instance info. diff --git a/src/backend/api/mod.rs b/src/backend/api/mod.rs index 2431a51..0169b3a 100644 --- a/src/backend/api/mod.rs +++ b/src/backend/api/mod.rs @@ -1,28 +1,42 @@ -use crate::backend::api::article::{ - create_article, list_articles, resolve_article, search_article, +use crate::{ + backend::{ + api::{ + article::{ + create_article, + edit_article, + fork_article, + get_article, + list_articles, + protect_article, + resolve_article, + search_article, + }, + instance::{follow_instance, get_local_instance, resolve_instance}, + user::{ + get_user, + login_user, + logout_user, + my_profile, + register_user, + validate, + AUTH_COOKIE, + }, + }, + database::{conflict::DbConflict, IbisData}, + error::MyResult, + }, + common::{ApiConflict, LocalUserView}, }; -use crate::backend::api::article::{edit_article, fork_article, get_article}; -use crate::backend::api::instance::get_local_instance; -use crate::backend::api::instance::{follow_instance, resolve_instance}; -use crate::backend::api::user::validate; -use crate::backend::api::user::{get_user, register_user}; -use crate::backend::api::user::{login_user, logout_user}; -use crate::backend::api::user::{my_profile, AUTH_COOKIE}; -use crate::backend::database::conflict::DbConflict; -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::common::ApiConflict; -use crate::common::LocalUserView; use activitypub_federation::config::Data; -use axum::routing::{get, post}; use axum::{ - http::Request, - http::StatusCode, + http::{Request, StatusCode}, middleware::{self, Next}, response::Response, + routing::{get, post}, Extension, + Json, + Router, }; -use axum::{Json, Router}; use axum_extra::extract::CookieJar; use axum_macros::debug_handler; use futures::future::try_join_all; @@ -40,6 +54,7 @@ pub fn api_routes() -> Router { .route("/article/list", get(list_articles)) .route("/article/fork", post(fork_article)) .route("/article/resolve", get(resolve_article)) + .route("/article/protect", post(protect_article)) .route("/edit_conflicts", get(edit_conflicts)) .route("/instance", get(get_local_instance)) .route("/instance/follow", post(follow_instance)) diff --git a/src/backend/api/user.rs b/src/backend/api/user.rs index 19f7bee..79f3e54 100644 --- a/src/backend/api/user.rs +++ b/src/backend/api/user.rs @@ -1,18 +1,26 @@ -use crate::backend::database::{read_jwt_secret, IbisData}; -use crate::backend::error::MyResult; -use crate::common::{DbPerson, GetUserData, LocalUserView, LoginUserData, RegisterUserData}; +use crate::{ + backend::{ + database::{read_jwt_secret, IbisData}, + error::MyResult, + }, + common::{DbPerson, GetUserForm, LocalUserView, LoginUserForm, RegisterUserForm}, +}; use activitypub_federation::config::Data; use anyhow::anyhow; -use axum::extract::Query; -use axum::{Form, Json}; +use axum::{extract::Query, Form, Json}; use axum_extra::extract::cookie::{Cookie, CookieJar, Expiration, SameSite}; use axum_macros::debug_handler; use bcrypt::verify; use chrono::Utc; -use jsonwebtoken::DecodingKey; -use jsonwebtoken::Validation; -use jsonwebtoken::{decode, get_current_timestamp}; -use jsonwebtoken::{encode, EncodingKey, Header}; +use jsonwebtoken::{ + decode, + encode, + get_current_timestamp, + DecodingKey, + EncodingKey, + Header, + Validation, +}; use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; @@ -57,7 +65,7 @@ pub async fn validate(jwt: &str, data: &Data) -> MyResult, jar: CookieJar, - Form(form): Form, + Form(form): Form, ) -> MyResult<(CookieJar, Json)> { if !data.config.registration_open { return Err(anyhow!("Registration is closed").into()); @@ -72,7 +80,7 @@ pub(in crate::backend::api) async fn register_user( pub(in crate::backend::api) async fn login_user( data: Data, jar: CookieJar, - Form(form): Form, + Form(form): Form, ) -> MyResult<(CookieJar, Json)> { let user = DbPerson::read_local_from_name(&form.username, &data)?; let valid = verify(&form.password, &user.local_user.password_encrypted)?; @@ -126,7 +134,7 @@ pub(in crate::backend::api) async fn logout_user( #[debug_handler] pub(in crate::backend::api) async fn get_user( - params: Query, + params: Query, data: Data, ) -> MyResult> { Ok(Json(DbPerson::read_from_name( diff --git a/src/backend/database/article.rs b/src/backend/database/article.rs index 1ec6749..a46ce1a 100644 --- a/src/backend/database/article.rs +++ b/src/backend/database/article.rs @@ -1,17 +1,24 @@ -use crate::backend::database::schema::{article, edit, instance}; -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::backend::federation::objects::edits_collection::DbEditCollection; -use crate::common::DbEdit; -use crate::common::EditVersion; -use crate::common::{ArticleView, DbArticle}; -use activitypub_federation::fetch::collection_id::CollectionId; -use activitypub_federation::fetch::object_id::ObjectId; -use diesel::dsl::max; - -use diesel::ExpressionMethods; +use crate::{ + backend::{ + database::{ + schema::{article, edit, instance}, + IbisData, + }, + error::MyResult, + federation::objects::edits_collection::DbEditCollection, + }, + common::{ArticleView, DbArticle, DbEdit, EditVersion}, +}; +use activitypub_federation::fetch::{collection_id::CollectionId, object_id::ObjectId}; use diesel::{ - insert_into, AsChangeset, BoolExpressionMethods, Insertable, PgTextExpressionMethods, QueryDsl, + dsl::max, + insert_into, + AsChangeset, + BoolExpressionMethods, + ExpressionMethods, + Insertable, + PgTextExpressionMethods, + QueryDsl, RunQueryDsl, }; use std::ops::DerefMut; @@ -24,6 +31,7 @@ pub struct DbArticleForm { pub ap_id: ObjectId, pub instance_id: i32, pub local: bool, + pub protected: bool, } // TODO: get rid of unnecessary methods @@ -58,6 +66,13 @@ impl DbArticle { .get_result::(conn.deref_mut())?) } + pub fn update_protected(id: i32, locked: bool, data: &IbisData) -> MyResult { + let mut conn = data.db_pool.get()?; + Ok(diesel::update(article::dsl::article.find(id)) + .set(article::dsl::protected.eq(locked)) + .get_result::(conn.deref_mut())?) + } + pub fn read(id: i32, data: &IbisData) -> MyResult { let mut conn = data.db_pool.get()?; Ok(article::table.find(id).get_result(conn.deref_mut())?) diff --git a/src/backend/database/conflict.rs b/src/backend/database/conflict.rs index e75c4c8..f2d3d19 100644 --- a/src/backend/database/conflict.rs +++ b/src/backend/database/conflict.rs @@ -1,16 +1,23 @@ -use crate::backend::database::schema::conflict; -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::backend::federation::activities::submit_article_update; -use crate::backend::utils::generate_article_version; -use crate::common::DbEdit; -use crate::common::DbLocalUser; -use crate::common::EditVersion; -use crate::common::{ApiConflict, DbArticle}; +use crate::{ + backend::{ + database::{schema::conflict, IbisData}, + error::MyResult, + federation::activities::submit_article_update, + utils::generate_article_version, + }, + common::{ApiConflict, DbArticle, DbEdit, DbLocalUser, EditVersion}, +}; use activitypub_federation::config::Data; -use diesel::ExpressionMethods; use diesel::{ - delete, insert_into, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable, + delete, + insert_into, + ExpressionMethods, + Identifiable, + Insertable, + QueryDsl, + Queryable, + RunQueryDsl, + Selectable, }; use diffy::{apply, merge, Patch}; use serde::{Deserialize, Serialize}; diff --git a/src/backend/database/edit.rs b/src/backend/database/edit.rs index f94ec40..755fe5b 100644 --- a/src/backend/database/edit.rs +++ b/src/backend/database/edit.rs @@ -1,12 +1,14 @@ -use crate::backend::database::schema::{edit, person}; -use crate::backend::error::MyResult; -use crate::backend::IbisData; -use crate::common::{DbArticle, DbEdit}; -use crate::common::{EditVersion, EditView}; +use crate::{ + backend::{ + database::schema::{edit, person}, + error::MyResult, + IbisData, + }, + common::{DbArticle, DbEdit, EditVersion, EditView}, +}; use activitypub_federation::fetch::object_id::ObjectId; use chrono::{DateTime, Utc}; -use diesel::ExpressionMethods; -use diesel::{insert_into, AsChangeset, Insertable, QueryDsl, RunQueryDsl}; +use diesel::{insert_into, AsChangeset, ExpressionMethods, Insertable, QueryDsl, RunQueryDsl}; use diffy::create_patch; use std::ops::DerefMut; diff --git a/src/backend/database/instance.rs b/src/backend/database/instance.rs index 8a9bd21..c67b92a 100644 --- a/src/backend/database/instance.rs +++ b/src/backend/database/instance.rs @@ -1,16 +1,29 @@ -use crate::backend::database::schema::{instance, instance_follow}; -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::backend::federation::objects::articles_collection::DbArticleCollection; -use crate::common::{DbInstance, DbPerson, InstanceView}; -use activitypub_federation::config::Data; -use activitypub_federation::fetch::collection_id::CollectionId; -use activitypub_federation::fetch::object_id::ObjectId; +use crate::{ + backend::{ + database::{ + schema::{instance, instance_follow}, + IbisData, + }, + error::MyResult, + federation::objects::articles_collection::DbArticleCollection, + }, + common::{DbInstance, DbPerson, InstanceView}, +}; +use activitypub_federation::{ + config::Data, + fetch::{collection_id::CollectionId, object_id::ObjectId}, +}; use chrono::{DateTime, Utc}; -use diesel::ExpressionMethods; -use diesel::{insert_into, AsChangeset, Insertable, JoinOnDsl, QueryDsl, RunQueryDsl}; -use std::fmt::Debug; -use std::ops::DerefMut; +use diesel::{ + insert_into, + AsChangeset, + ExpressionMethods, + Insertable, + JoinOnDsl, + QueryDsl, + RunQueryDsl, +}; +use std::{fmt::Debug, ops::DerefMut}; #[derive(Debug, Clone, Insertable, AsChangeset)] #[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))] diff --git a/src/backend/database/mod.rs b/src/backend/database/mod.rs index d2d2c0a..8c9623e 100644 --- a/src/backend/database/mod.rs +++ b/src/backend/database/mod.rs @@ -1,11 +1,10 @@ -use crate::backend::config::IbisConfig; -use crate::backend::database::schema::jwt_secret; -use crate::backend::error::MyResult; -use diesel::r2d2::ConnectionManager; -use diesel::r2d2::Pool; -use diesel::PgConnection; -use diesel::{QueryDsl, RunQueryDsl}; - +use crate::backend::{config::IbisConfig, database::schema::jwt_secret, error::MyResult}; +use diesel::{ + r2d2::{ConnectionManager, Pool}, + PgConnection, + QueryDsl, + RunQueryDsl, +}; use std::ops::DerefMut; pub mod article; diff --git a/src/backend/database/schema.rs b/src/backend/database/schema.rs index e97310f..c6179d7 100644 --- a/src/backend/database/schema.rs +++ b/src/backend/database/schema.rs @@ -9,6 +9,7 @@ diesel::table! { ap_id -> Varchar, instance_id -> Int4, local -> Bool, + protected -> Bool, } } diff --git a/src/backend/database/user.rs b/src/backend/database/user.rs index b4b9977..3f43e69 100644 --- a/src/backend/database/user.rs +++ b/src/backend/database/user.rs @@ -1,18 +1,30 @@ -use crate::backend::database::schema::{instance, instance_follow}; -use crate::backend::database::schema::{local_user, person}; -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::common::utils::http_protocol_str; -use crate::common::{DbInstance, DbLocalUser, DbPerson, LocalUserView}; -use activitypub_federation::config::Data; -use activitypub_federation::fetch::object_id::ObjectId; -use activitypub_federation::http_signatures::generate_actor_keypair; -use bcrypt::hash; -use bcrypt::DEFAULT_COST; +use crate::{ + backend::{ + database::{ + schema::{instance, instance_follow, local_user, person}, + IbisData, + }, + error::MyResult, + }, + common::{utils::http_protocol_str, DbInstance, DbLocalUser, DbPerson, LocalUserView}, +}; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + http_signatures::generate_actor_keypair, +}; +use bcrypt::{hash, DEFAULT_COST}; use chrono::{DateTime, Local, Utc}; -use diesel::{insert_into, AsChangeset, Insertable, RunQueryDsl}; -use diesel::{ExpressionMethods, JoinOnDsl}; -use diesel::{PgTextExpressionMethods, QueryDsl}; +use diesel::{ + insert_into, + AsChangeset, + ExpressionMethods, + Insertable, + JoinOnDsl, + PgTextExpressionMethods, + QueryDsl, + RunQueryDsl, +}; use std::ops::DerefMut; #[derive(Debug, Clone, Insertable, AsChangeset)] diff --git a/src/backend/federation/activities/accept.rs b/src/backend/federation/activities/accept.rs index f3ec534..3c512b1 100644 --- a/src/backend/federation/activities/accept.rs +++ b/src/backend/federation/activities/accept.rs @@ -1,11 +1,17 @@ -use crate::backend::error::MyResult; -use crate::backend::federation::send_activity; -use crate::backend::utils::generate_activity_id; -use crate::backend::{database::IbisData, federation::activities::follow::Follow}; -use crate::common::DbInstance; -use activitypub_federation::traits::Actor; +use crate::{ + backend::{ + database::IbisData, + error::MyResult, + federation::{activities::follow::Follow, send_activity}, + utils::generate_activity_id, + }, + common::DbInstance, +}; use activitypub_federation::{ - config::Data, fetch::object_id::ObjectId, kinds::activity::AcceptType, traits::ActivityHandler, + config::Data, + fetch::object_id::ObjectId, + kinds::activity::AcceptType, + traits::{ActivityHandler, Actor}, }; use serde::{Deserialize, Serialize}; use url::Url; diff --git a/src/backend/federation/activities/create_article.rs b/src/backend/federation/activities/create_article.rs index 5eee755..8b243a3 100644 --- a/src/backend/federation/activities/create_article.rs +++ b/src/backend/federation/activities/create_article.rs @@ -1,13 +1,16 @@ -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::backend::federation::objects::article::ApubArticle; -use crate::backend::utils::generate_activity_id; -use crate::common::DbArticle; -use crate::common::DbInstance; -use activitypub_federation::kinds::activity::CreateType; +use crate::{ + backend::{ + database::IbisData, + error::MyResult, + federation::objects::article::ApubArticle, + utils::generate_activity_id, + }, + common::{DbArticle, DbInstance}, +}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, + kinds::activity::CreateType, protocol::helpers::deserialize_one_or_many, traits::{ActivityHandler, Object}, }; diff --git a/src/backend/federation/activities/follow.rs b/src/backend/federation/activities/follow.rs index b5553b4..a68cb32 100644 --- a/src/backend/federation/activities/follow.rs +++ b/src/backend/federation/activities/follow.rs @@ -1,15 +1,17 @@ -use crate::backend::error::MyResult; -use crate::backend::federation::send_activity; -use crate::backend::{ - database::IbisData, federation::activities::accept::Accept, generate_activity_id, +use crate::{ + backend::{ + database::IbisData, + error::MyResult, + federation::{activities::accept::Accept, send_activity}, + generate_activity_id, + }, + common::{DbInstance, DbPerson}, }; -use crate::common::DbInstance; -use crate::common::DbPerson; -use activitypub_federation::protocol::verification::verify_urls_match; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, kinds::activity::FollowType, + protocol::verification::verify_urls_match, traits::{ActivityHandler, Actor}, }; use serde::{Deserialize, Serialize}; diff --git a/src/backend/federation/activities/mod.rs b/src/backend/federation/activities/mod.rs index 9fc1f8e..251d546 100644 --- a/src/backend/federation/activities/mod.rs +++ b/src/backend/federation/activities/mod.rs @@ -1,11 +1,14 @@ -use crate::backend::database::edit::DbEditForm; -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; -use crate::backend::federation::activities::update_remote_article::UpdateRemoteArticle; -use crate::common::DbInstance; -use crate::common::EditVersion; -use crate::common::{DbArticle, DbEdit}; +use crate::{ + backend::{ + database::{edit::DbEditForm, IbisData}, + error::Error, + federation::activities::{ + update_local_article::UpdateLocalArticle, + update_remote_article::UpdateRemoteArticle, + }, + }, + common::{DbArticle, DbEdit, DbInstance, EditVersion}, +}; use activitypub_federation::config::Data; use chrono::Utc; diff --git a/src/backend/federation/activities/reject.rs b/src/backend/federation/activities/reject.rs index 8afff89..4a54ba5 100644 --- a/src/backend/federation/activities/reject.rs +++ b/src/backend/federation/activities/reject.rs @@ -1,17 +1,22 @@ -use crate::backend::database::conflict::{DbConflict, DbConflictForm}; -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::backend::federation::objects::edit::ApubEdit; -use crate::backend::utils::generate_activity_id; -use crate::common::DbInstance; -use crate::common::EditVersion; -use activitypub_federation::kinds::activity::RejectType; +use crate::{ + backend::{ + database::{ + conflict::{DbConflict, DbConflictForm}, + IbisData, + }, + error::MyResult, + federation::{objects::edit::ApubEdit, send_activity}, + utils::generate_activity_id, + }, + common::{DbInstance, EditVersion}, +}; use activitypub_federation::{ - config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, + config::Data, + fetch::object_id::ObjectId, + kinds::activity::RejectType, + protocol::helpers::deserialize_one_or_many, traits::ActivityHandler, }; - -use crate::backend::federation::send_activity; use serde::{Deserialize, Serialize}; use url::Url; diff --git a/src/backend/federation/activities/update_local_article.rs b/src/backend/federation/activities/update_local_article.rs index 9983d48..51bea0f 100644 --- a/src/backend/federation/activities/update_local_article.rs +++ b/src/backend/federation/activities/update_local_article.rs @@ -1,14 +1,16 @@ -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; -use crate::backend::federation::objects::article::ApubArticle; - -use crate::backend::utils::generate_activity_id; -use crate::common::DbArticle; -use crate::common::DbInstance; -use activitypub_federation::kinds::activity::UpdateType; +use crate::{ + backend::{ + database::IbisData, + error::MyResult, + federation::objects::article::ApubArticle, + utils::generate_activity_id, + }, + common::{DbArticle, DbInstance}, +}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, + kinds::activity::UpdateType, protocol::helpers::deserialize_one_or_many, traits::{ActivityHandler, Object}, }; diff --git a/src/backend/federation/activities/update_remote_article.rs b/src/backend/federation/activities/update_remote_article.rs index d569c46..64f17d5 100644 --- a/src/backend/federation/activities/update_remote_article.rs +++ b/src/backend/federation/activities/update_remote_article.rs @@ -1,19 +1,20 @@ -use crate::backend::database::IbisData; -use crate::backend::error::MyResult; - -use crate::backend::federation::activities::reject::RejectEdit; -use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; -use crate::backend::federation::objects::edit::ApubEdit; -use crate::backend::federation::send_activity; -use crate::backend::utils::generate_activity_id; -use crate::common::validation::can_edit_article; -use crate::common::DbArticle; -use crate::common::DbEdit; -use crate::common::DbInstance; -use activitypub_federation::kinds::activity::UpdateType; +use crate::{ + backend::{ + database::IbisData, + error::MyResult, + federation::{ + activities::{reject::RejectEdit, update_local_article::UpdateLocalArticle}, + objects::edit::ApubEdit, + send_activity, + }, + utils::generate_activity_id, + }, + common::{validation::can_edit_article, DbArticle, DbEdit, DbInstance}, +}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, + kinds::activity::UpdateType, protocol::helpers::deserialize_one_or_many, traits::{ActivityHandler, Object}, }; diff --git a/src/backend/federation/mod.rs b/src/backend/federation/mod.rs index 8e5e7f5..6a7c9a8 100644 --- a/src/backend/federation/mod.rs +++ b/src/backend/federation/mod.rs @@ -1,10 +1,11 @@ -use crate::backend::config::IbisConfig; -use crate::backend::database::IbisData; -use activitypub_federation::activity_queue::queue_activity; -use activitypub_federation::config::{Data, UrlVerifier}; -use activitypub_federation::error::Error as ActivityPubError; -use activitypub_federation::protocol::context::WithContext; -use activitypub_federation::traits::{ActivityHandler, Actor}; +use crate::backend::{config::IbisConfig, database::IbisData}; +use activitypub_federation::{ + activity_queue::queue_activity, + config::{Data, UrlVerifier}, + error::Error as ActivityPubError, + protocol::context::WithContext, + traits::{ActivityHandler, Actor}, +}; use async_trait::async_trait; use serde::Serialize; use std::fmt::Debug; diff --git a/src/backend/federation/objects/article.rs b/src/backend/federation/objects/article.rs index c0b1c09..84d4503 100644 --- a/src/backend/federation/objects/article.rs +++ b/src/backend/federation/objects/article.rs @@ -1,17 +1,17 @@ -use crate::backend::database::article::DbArticleForm; -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::backend::federation::objects::edits_collection::DbEditCollection; -use crate::common::DbArticle; -use crate::common::DbInstance; -use crate::common::EditVersion; -use activitypub_federation::config::Data; -use activitypub_federation::fetch::collection_id::CollectionId; -use activitypub_federation::kinds::object::ArticleType; -use activitypub_federation::kinds::public; -use activitypub_federation::protocol::verification::verify_domains_match; +use crate::{ + backend::{ + database::{article::DbArticleForm, IbisData}, + error::Error, + federation::objects::edits_collection::DbEditCollection, + }, + common::{DbArticle, DbInstance, EditVersion}, +}; use activitypub_federation::{ - fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, traits::Object, + config::Data, + fetch::{collection_id::CollectionId, object_id::ObjectId}, + kinds::{object::ArticleType, public}, + protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match}, + traits::Object, }; use serde::{Deserialize, Serialize}; use url::Url; @@ -29,6 +29,7 @@ pub struct ApubArticle { latest_version: EditVersion, content: String, name: String, + protected: bool, } #[async_trait::async_trait] @@ -56,6 +57,7 @@ impl Object for DbArticle { latest_version: self.latest_edit_version(data)?, content: self.text, name: self.title, + protected: self.protected, }) } @@ -76,6 +78,7 @@ impl Object for DbArticle { ap_id: json.id, local: false, instance_id: instance.id, + protected: json.protected, }; let article = DbArticle::create_or_update(form, data)?; diff --git a/src/backend/federation/objects/articles_collection.rs b/src/backend/federation/objects/articles_collection.rs index 1275db0..8141494 100644 --- a/src/backend/federation/objects/articles_collection.rs +++ b/src/backend/federation/objects/articles_collection.rs @@ -1,17 +1,14 @@ -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::backend::federation::objects::article::ApubArticle; -use crate::common::DbInstance; - -use crate::common::DbArticle; -use activitypub_federation::kinds::collection::CollectionType; -use activitypub_federation::protocol::verification::verify_domains_match; +use crate::{ + backend::{database::IbisData, error::Error, federation::objects::article::ApubArticle}, + common::{DbArticle, DbInstance}, +}; use activitypub_federation::{ config::Data, + kinds::collection::CollectionType, + protocol::verification::verify_domains_match, traits::{Collection, Object}, }; -use futures::future; -use futures::future::try_join_all; +use futures::{future, future::try_join_all}; use serde::{Deserialize, Serialize}; use url::Url; diff --git a/src/backend/federation/objects/edit.rs b/src/backend/federation/objects/edit.rs index 1b39143..b320b09 100644 --- a/src/backend/federation/objects/edit.rs +++ b/src/backend/federation/objects/edit.rs @@ -1,13 +1,16 @@ -use crate::backend::database::edit::DbEditForm; -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::common::DbPerson; -use crate::common::EditVersion; -use crate::common::{DbArticle, DbEdit}; -use activitypub_federation::config::Data; -use activitypub_federation::fetch::object_id::ObjectId; -use activitypub_federation::protocol::verification::verify_domains_match; -use activitypub_federation::traits::Object; +use crate::{ + backend::{ + database::{edit::DbEditForm, IbisData}, + error::Error, + }, + common::{DbArticle, DbEdit, DbPerson, EditVersion}, +}; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + protocol::verification::verify_domains_match, + traits::Object, +}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use url::Url; diff --git a/src/backend/federation/objects/edits_collection.rs b/src/backend/federation/objects/edits_collection.rs index 5c34281..93fa91e 100644 --- a/src/backend/federation/objects/edits_collection.rs +++ b/src/backend/federation/objects/edits_collection.rs @@ -1,18 +1,14 @@ -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::backend::federation::objects::edit::ApubEdit; -use crate::common::DbArticle; - -use crate::common::DbEdit; -use crate::common::DbInstance; -use activitypub_federation::kinds::collection::OrderedCollectionType; -use activitypub_federation::protocol::verification::verify_domains_match; +use crate::{ + backend::{database::IbisData, error::Error, federation::objects::edit::ApubEdit}, + common::{DbArticle, DbEdit, DbInstance}, +}; use activitypub_federation::{ config::Data, + kinds::collection::OrderedCollectionType, + protocol::verification::verify_domains_match, traits::{Collection, Object}, }; -use futures::future; -use futures::future::try_join_all; +use futures::{future, future::try_join_all}; use serde::{Deserialize, Serialize}; use url::Url; diff --git a/src/backend/federation/objects/instance.rs b/src/backend/federation/objects/instance.rs index f8cd3d5..df8f4e7 100644 --- a/src/backend/federation/objects/instance.rs +++ b/src/backend/federation/objects/instance.rs @@ -1,19 +1,17 @@ -use crate::backend::database::instance::DbInstanceForm; -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::backend::error::MyResult; -use crate::backend::federation::objects::articles_collection::DbArticleCollection; -use crate::backend::federation::send_activity; -use crate::common::utils::extract_domain; -use crate::common::DbInstance; -use activitypub_federation::fetch::collection_id::CollectionId; -use activitypub_federation::kinds::actor::ServiceType; -use activitypub_federation::traits::ActivityHandler; +use crate::{ + backend::{ + database::{instance::DbInstanceForm, IbisData}, + error::{Error, MyResult}, + federation::{objects::articles_collection::DbArticleCollection, send_activity}, + }, + common::{utils::extract_domain, DbInstance}, +}; use activitypub_federation::{ config::Data, - fetch::object_id::ObjectId, + fetch::{collection_id::CollectionId, object_id::ObjectId}, + kinds::actor::ServiceType, protocol::{public_key::PublicKey, verification::verify_domains_match}, - traits::{Actor, Object}, + traits::{ActivityHandler, Actor, Object}, }; use chrono::{DateTime, Local, Utc}; use serde::{Deserialize, Serialize}; diff --git a/src/backend/federation/objects/user.rs b/src/backend/federation/objects/user.rs index e2ae239..e04c14b 100644 --- a/src/backend/federation/objects/user.rs +++ b/src/backend/federation/objects/user.rs @@ -1,11 +1,14 @@ -use crate::backend::database::user::DbPersonForm; -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::common::DbPerson; -use activitypub_federation::kinds::actor::PersonType; +use crate::{ + backend::{ + database::{user::DbPersonForm, IbisData}, + error::Error, + }, + common::DbPerson, +}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, + kinds::actor::PersonType, protocol::{public_key::PublicKey, verification::verify_domains_match}, traits::{Actor, Object}, }; diff --git a/src/backend/federation/routes.rs b/src/backend/federation/routes.rs index 3c9b881..7c091f5 100644 --- a/src/backend/federation/routes.rs +++ b/src/backend/federation/routes.rs @@ -1,33 +1,42 @@ -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::backend::error::MyResult; -use crate::backend::federation::activities::accept::Accept; -use crate::backend::federation::activities::create_article::CreateArticle; -use crate::backend::federation::activities::follow::Follow; -use crate::backend::federation::activities::reject::RejectEdit; -use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; -use crate::backend::federation::activities::update_remote_article::UpdateRemoteArticle; -use crate::backend::federation::objects::article::ApubArticle; -use crate::backend::federation::objects::articles_collection::{ - ArticleCollection, DbArticleCollection, +use crate::{ + backend::{ + database::IbisData, + error::{Error, MyResult}, + federation::{ + activities::{ + accept::Accept, + create_article::CreateArticle, + follow::Follow, + reject::RejectEdit, + update_local_article::UpdateLocalArticle, + update_remote_article::UpdateRemoteArticle, + }, + objects::{ + article::ApubArticle, + articles_collection::{ArticleCollection, DbArticleCollection}, + edits_collection::{ApubEditCollection, DbEditCollection}, + instance::ApubInstance, + user::ApubUser, + }, + }, + }, + common::{DbArticle, DbInstance, DbPerson}, +}; +use activitypub_federation::{ + axum::{ + inbox::{receive_activity, ActivityData}, + json::FederationJson, + }, + config::Data, + protocol::context::WithContext, + traits::{ActivityHandler, Actor, Collection, Object}, +}; +use axum::{ + extract::Path, + response::IntoResponse, + routing::{get, post}, + Router, }; -use crate::backend::federation::objects::edits_collection::{ApubEditCollection, DbEditCollection}; -use crate::backend::federation::objects::instance::ApubInstance; -use crate::backend::federation::objects::user::ApubUser; -use crate::common::DbArticle; -use crate::common::DbInstance; -use crate::common::DbPerson; -use activitypub_federation::axum::inbox::{receive_activity, ActivityData}; -use activitypub_federation::axum::json::FederationJson; -use activitypub_federation::config::Data; -use activitypub_federation::protocol::context::WithContext; -use activitypub_federation::traits::Actor; -use activitypub_federation::traits::Object; -use activitypub_federation::traits::{ActivityHandler, Collection}; -use axum::extract::Path; -use axum::response::IntoResponse; -use axum::routing::{get, post}; -use axum::Router; use axum_macros::debug_handler; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 388ea2c..d19ef5b 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,38 +1,45 @@ -use crate::backend::config::IbisConfig; -use crate::backend::database::article::DbArticleForm; -use crate::backend::database::instance::DbInstanceForm; -use crate::backend::database::IbisData; -use crate::backend::error::Error; -use crate::backend::error::MyResult; -use crate::backend::federation::activities::submit_article_update; -use crate::backend::federation::routes::federation_routes; -use crate::backend::federation::VerifyUrlData; -use crate::backend::utils::generate_activity_id; -use crate::common::utils::http_protocol_str; -use crate::common::{DbArticle, DbInstance, DbPerson, EditVersion, MAIN_PAGE_NAME}; -use crate::frontend::app::App; -use activitypub_federation::config::{Data, FederationConfig, FederationMiddleware}; -use activitypub_federation::fetch::collection_id::CollectionId; -use activitypub_federation::fetch::object_id::ObjectId; -use activitypub_federation::http_signatures::generate_actor_keypair; +use crate::{ + backend::{ + config::IbisConfig, + database::{article::DbArticleForm, instance::DbInstanceForm, IbisData}, + error::{Error, MyResult}, + federation::{activities::submit_article_update, routes::federation_routes, VerifyUrlData}, + utils::generate_activity_id, + }, + common::{ + utils::http_protocol_str, + DbArticle, + DbInstance, + DbPerson, + EditVersion, + MAIN_PAGE_NAME, + }, + frontend::app::App, +}; +use activitypub_federation::{ + config::{Data, FederationConfig, FederationMiddleware}, + fetch::{collection_id::CollectionId, object_id::ObjectId}, + http_signatures::generate_actor_keypair, +}; use api::api_routes; -use axum::debug_handler; -use axum::headers::HeaderMap; -use axum::http::{HeaderValue, Request}; -use axum::response::IntoResponse; -use axum::routing::get; -use axum::Server; -use axum::ServiceExt; -use axum::{middleware::Next, response::Response, Router}; +use axum::{ + debug_handler, + headers::HeaderMap, + http::{HeaderValue, Request}, + middleware::Next, + response::{IntoResponse, Response}, + routing::get, + Router, + Server, + ServiceExt, +}; use chrono::Local; -use diesel::r2d2::ConnectionManager; -use diesel::r2d2::Pool; -use diesel::PgConnection; -use diesel_migrations::embed_migrations; -use diesel_migrations::EmbeddedMigrations; -use diesel_migrations::MigrationHarness; -use leptos::leptos_config::get_config_from_str; -use leptos::*; +use diesel::{ + r2d2::{ConnectionManager, Pool}, + PgConnection, +}; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use leptos::{leptos_config::get_config_from_str, *}; use leptos_axum::{generate_route_list, LeptosRoutes}; use log::info; use tower::Layer; @@ -179,6 +186,7 @@ async fn setup(data: &Data) -> Result<(), Error> { ))?, instance_id: instance.id, local: true, + protected: true, }; let article = DbArticle::create(form, data)?; // also create an article so its included in most recently edited list diff --git a/src/backend/utils.rs b/src/backend/utils.rs index 2ec90ad..18a7a46 100644 --- a/src/backend/utils.rs +++ b/src/backend/utils.rs @@ -1,14 +1,12 @@ -use crate::backend::error::MyResult; -use crate::common::EditView; -use crate::common::{utils, EditVersion}; -use activitypub_federation::fetch::object_id::ObjectId; -use activitypub_federation::traits::Object; +use crate::{ + backend::error::MyResult, + common::{utils, utils::extract_domain, EditVersion, EditView}, +}; +use activitypub_federation::{fetch::object_id::ObjectId, traits::Object}; use anyhow::anyhow; use diffy::{apply, Patch}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::Deserialize; - -use crate::common::utils::extract_domain; use url::{ParseError, Url}; pub fn generate_activity_id(for_url: &ObjectId) -> Result diff --git a/src/common/mod.rs b/src/common/mod.rs index 5ec0775..c89be24 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -20,14 +20,14 @@ pub const MAIN_PAGE_NAME: &str = "Main_Page"; /// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66 #[derive(Deserialize, Serialize, Clone, Debug)] -pub struct GetArticleData { +pub struct GetArticleForm { pub title: Option, pub domain: Option, pub id: Option, } #[derive(Deserialize, Serialize, Clone)] -pub struct ListArticlesData { +pub struct ListArticlesForm { pub only_local: Option, } @@ -53,6 +53,7 @@ pub struct DbArticle { pub ap_id: String, pub instance_id: i32, pub local: bool, + pub protected: bool, } /// Represents a single change to the article. @@ -115,13 +116,13 @@ impl Default for EditVersion { } #[derive(Deserialize, Serialize, Clone)] -pub struct RegisterUserData { +pub struct RegisterUserForm { pub username: String, pub password: String, } #[derive(Deserialize, Serialize)] -pub struct LoginUserData { +pub struct LoginUserForm { pub username: String, pub password: String, } @@ -175,14 +176,14 @@ impl DbPerson { } #[derive(Deserialize, Serialize)] -pub struct CreateArticleData { +pub struct CreateArticleForm { pub title: String, pub text: String, pub summary: String, } #[derive(Deserialize, Serialize, Debug)] -pub struct EditArticleData { +pub struct EditArticleForm { /// Id of the article to edit pub article_id: i32, /// Full, new text of the article. A diff against `previous_version` is generated on the backend @@ -197,8 +198,14 @@ pub struct EditArticleData { pub resolve_conflict_id: Option, } +#[derive(Deserialize, Serialize, Debug)] +pub struct ProtectArticleForm { + pub article_id: i32, + pub protected: bool, +} + #[derive(Deserialize, Serialize)] -pub struct ForkArticleData { +pub struct ForkArticleForm { pub article_id: i32, pub new_title: String, } @@ -209,7 +216,7 @@ pub struct FollowInstance { } #[derive(Deserialize, Serialize, Clone)] -pub struct SearchArticleData { +pub struct SearchArticleForm { pub query: String, } @@ -269,7 +276,7 @@ pub struct InstanceView { } #[derive(Deserialize, Serialize, Clone, Debug)] -pub struct GetUserData { +pub struct GetUserForm { pub name: String, pub domain: Option, } diff --git a/src/common/validation.rs b/src/common/validation.rs index 458c5c1..f2278b7 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -1,14 +1,14 @@ -use crate::common::{DbArticle, MAIN_PAGE_NAME}; -use anyhow::anyhow; -use anyhow::Result; +use crate::common::DbArticle; +use anyhow::{anyhow, Result}; pub fn can_edit_article(article: &DbArticle, is_admin: bool) -> Result<()> { - if article.title == MAIN_PAGE_NAME { + let err = anyhow!("Article is protected, only admins on origin instance can edit"); + if article.protected { if !article.local { - return Err(anyhow!("Cannot edit main page of remote instance")); + return Err(err); } - if article.local && !is_admin { - return Err(anyhow!("Only admin can edit main page")); + if !is_admin { + return Err(err); } } Ok(()) diff --git a/src/database/schema.rs b/src/database/schema.rs new file mode 100644 index 0000000..c6179d7 --- /dev/null +++ b/src/database/schema.rs @@ -0,0 +1,117 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + article (id) { + id -> Int4, + title -> Text, + text -> Text, + #[max_length = 255] + ap_id -> Varchar, + instance_id -> Int4, + local -> Bool, + protected -> Bool, + } +} + +diesel::table! { + conflict (id) { + id -> Int4, + hash -> Uuid, + diff -> Text, + summary -> Text, + creator_id -> Int4, + article_id -> Int4, + previous_version_id -> Uuid, + } +} + +diesel::table! { + edit (id) { + id -> Int4, + creator_id -> Int4, + hash -> Uuid, + #[max_length = 255] + ap_id -> Varchar, + diff -> Text, + summary -> Text, + article_id -> Int4, + previous_version_id -> Uuid, + created -> Timestamptz, + } +} + +diesel::table! { + instance (id) { + id -> Int4, + domain -> Text, + #[max_length = 255] + ap_id -> Varchar, + description -> Nullable, + inbox_url -> Text, + #[max_length = 255] + articles_url -> Varchar, + public_key -> Text, + private_key -> Nullable, + last_refreshed_at -> Timestamptz, + local -> Bool, + } +} + +diesel::table! { + instance_follow (id) { + id -> Int4, + instance_id -> Int4, + follower_id -> Int4, + pending -> Bool, + } +} + +diesel::table! { + jwt_secret (id) { + id -> Int4, + secret -> Varchar, + } +} + +diesel::table! { + local_user (id) { + id -> Int4, + password_encrypted -> Text, + person_id -> Int4, + admin -> Bool, + } +} + +diesel::table! { + person (id) { + id -> Int4, + username -> Text, + #[max_length = 255] + ap_id -> Varchar, + inbox_url -> Text, + public_key -> Text, + private_key -> Nullable, + last_refreshed_at -> Timestamptz, + local -> Bool, + } +} + +diesel::joinable!(article -> instance (instance_id)); +diesel::joinable!(conflict -> article (article_id)); +diesel::joinable!(conflict -> local_user (creator_id)); +diesel::joinable!(edit -> article (article_id)); +diesel::joinable!(edit -> person (creator_id)); +diesel::joinable!(instance_follow -> instance (instance_id)); +diesel::joinable!(instance_follow -> person (follower_id)); +diesel::joinable!(local_user -> person (person_id)); + +diesel::allow_tables_to_appear_in_same_query!( + article, + conflict, + edit, + instance, + instance_follow, + jwt_secret, + local_user, + person, +); diff --git a/src/frontend/api.rs b/src/frontend/api.rs index b20e9ef..25afbf3 100644 --- a/src/frontend/api.rs +++ b/src/frontend/api.rs @@ -1,11 +1,28 @@ -use crate::common::utils::http_protocol_str; -use crate::common::{ApiConflict, ListArticlesData}; -use crate::common::{ArticleView, LoginUserData, RegisterUserData}; -use crate::common::{CreateArticleData, EditArticleData, ForkArticleData, LocalUserView}; -use crate::common::{DbArticle, GetArticleData}; -use crate::common::{DbInstance, FollowInstance, InstanceView, SearchArticleData}; -use crate::common::{DbPerson, GetUserData, ResolveObject}; -use crate::frontend::error::MyResult; +use crate::{ + common::{ + utils::http_protocol_str, + ApiConflict, + ArticleView, + CreateArticleForm, + DbArticle, + DbInstance, + DbPerson, + EditArticleForm, + FollowInstance, + ForkArticleForm, + GetArticleForm, + GetUserForm, + InstanceView, + ListArticlesForm, + LocalUserView, + LoginUserForm, + ProtectArticleForm, + RegisterUserForm, + ResolveObject, + SearchArticleForm, + }, + frontend::error::MyResult, +}; use anyhow::anyhow; use reqwest::{Client, RequestBuilder, StatusCode}; use serde::{Deserialize, Serialize}; @@ -34,15 +51,15 @@ impl ApiClient { handle_json_res::(req).await } - pub async fn get_article(&self, data: GetArticleData) -> MyResult { + pub async fn get_article(&self, data: GetArticleForm) -> MyResult { self.get_query("/api/v1/article", Some(data)).await } - pub async fn list_articles(&self, data: ListArticlesData) -> MyResult> { + pub async fn list_articles(&self, data: ListArticlesForm) -> MyResult> { self.get_query("/api/v1/article/list", Some(data)).await } - pub async fn register(&self, register_form: RegisterUserData) -> MyResult { + pub async fn register(&self, register_form: RegisterUserForm) -> MyResult { let req = self .client .post(self.request_endpoint("/api/v1/account/register")) @@ -50,7 +67,7 @@ impl ApiClient { handle_json_res::(req).await } - pub async fn login(&self, login_form: LoginUserData) -> MyResult { + pub async fn login(&self, login_form: LoginUserForm) -> MyResult { let req = self .client .post(self.request_endpoint("/api/v1/account/login")) @@ -58,7 +75,7 @@ impl ApiClient { handle_json_res::(req).await } - pub async fn create_article(&self, data: &CreateArticleData) -> MyResult { + pub async fn create_article(&self, data: &CreateArticleForm) -> MyResult { let req = self .client .post(self.request_endpoint("/api/v1/article")) @@ -68,7 +85,7 @@ impl ApiClient { pub async fn edit_article_with_conflict( &self, - edit_form: &EditArticleData, + edit_form: &EditArticleForm, ) -> MyResult> { let req = self .client @@ -77,11 +94,11 @@ impl ApiClient { handle_json_res(req).await } - pub async fn edit_article(&self, edit_form: &EditArticleData) -> MyResult { + pub async fn edit_article(&self, edit_form: &EditArticleForm) -> MyResult { let edit_res = self.edit_article_with_conflict(edit_form).await?; assert!(edit_res.is_none()); - self.get_article(GetArticleData { + self.get_article(GetArticleForm { title: None, domain: None, id: Some(edit_form.article_id), @@ -89,7 +106,7 @@ impl ApiClient { .await } - pub async fn search(&self, search_form: &SearchArticleData) -> MyResult> { + pub async fn search(&self, search_form: &SearchArticleForm) -> MyResult> { self.get_query("/api/v1/search", Some(search_form)).await } @@ -147,7 +164,7 @@ impl ApiClient { Ok(()) } - pub async fn fork_article(&self, form: &ForkArticleData) -> MyResult { + pub async fn fork_article(&self, form: &ForkArticleForm) -> MyResult { let req = self .client .post(self.request_endpoint("/api/v1/article/fork")) @@ -155,6 +172,14 @@ impl ApiClient { Ok(handle_json_res(req).await.unwrap()) } + pub async fn protect_article(&self, params: &ProtectArticleForm) -> MyResult { + let req = self + .client + .post(self.request_endpoint("/api/v1/article/protect")) + .form(params); + handle_json_res(req).await + } + pub async fn get_conflicts(&self) -> MyResult> { let req = self .client @@ -173,7 +198,7 @@ impl ApiClient { self.get_query("/api/v1/instance/resolve", Some(resolve_object)) .await } - pub async fn get_user(&self, data: GetUserData) -> MyResult { + pub async fn get_user(&self, data: GetUserForm) -> MyResult { self.get_query("/api/v1/user", Some(data)).await } diff --git a/src/frontend/app.rs b/src/frontend/app.rs index f42da6a..409cdc4 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -1,29 +1,44 @@ -use crate::common::LocalUserView; -use crate::frontend::api::ApiClient; -use crate::frontend::backend_hostname; -use crate::frontend::components::nav::Nav; -use crate::frontend::pages::article::actions::ArticleActions; -use crate::frontend::pages::article::create::CreateArticle; -use crate::frontend::pages::article::edit::EditArticle; -use crate::frontend::pages::article::history::ArticleHistory; -use crate::frontend::pages::article::list::ListArticles; -use crate::frontend::pages::article::read::ReadArticle; -use crate::frontend::pages::conflicts::Conflicts; -use crate::frontend::pages::diff::EditDiff; -use crate::frontend::pages::instance_details::InstanceDetails; -use crate::frontend::pages::login::Login; -use crate::frontend::pages::register::Register; -use crate::frontend::pages::search::Search; -use crate::frontend::pages::user_profile::UserProfile; -use leptos::{ - component, create_local_resource, create_rw_signal, expect_context, provide_context, - use_context, view, IntoView, RwSignal, SignalGet, SignalGetUntracked, SignalUpdate, +use crate::{ + common::LocalUserView, + frontend::{ + api::ApiClient, + backend_hostname, + components::nav::Nav, + pages::{ + article::{ + actions::ArticleActions, + create::CreateArticle, + edit::EditArticle, + history::ArticleHistory, + list::ListArticles, + read::ReadArticle, + }, + conflicts::Conflicts, + diff::EditDiff, + instance_details::InstanceDetails, + login::Login, + register::Register, + search::Search, + user_profile::UserProfile, + }, + }, }; -use leptos_meta::provide_meta_context; -use leptos_meta::*; -use leptos_router::Route; -use leptos_router::Router; -use leptos_router::Routes; +use leptos::{ + component, + create_local_resource, + create_rw_signal, + expect_context, + provide_context, + use_context, + view, + IntoView, + RwSignal, + SignalGet, + SignalGetUntracked, + SignalUpdate, +}; +use leptos_meta::{provide_meta_context, *}; +use leptos_router::{Route, Router, Routes}; use reqwest::Client; // https://book.leptos.dev/15_global_state.html diff --git a/src/frontend/components/article_nav.rs b/src/frontend/components/article_nav.rs index e548481..0198d1a 100644 --- a/src/frontend/components/article_nav.rs +++ b/src/frontend/components/article_nav.rs @@ -1,7 +1,7 @@ -use crate::common::validation::can_edit_article; -use crate::common::ArticleView; -use crate::frontend::app::GlobalState; -use crate::frontend::article_link; +use crate::{ + common::{validation::can_edit_article, ArticleView}, + frontend::{app::GlobalState, article_link}, +}; use leptos::*; use leptos_router::*; @@ -13,6 +13,7 @@ pub fn ArticleNav(article: Resource, ArticleView>) -> impl IntoVi {move || article.get().map(|article| { let article_link = article_link(&article.article); let article_link_ = article_link.clone(); + let protected = article.article.protected; view!{ }})} diff --git a/src/frontend/components/nav.rs b/src/frontend/components/nav.rs index a32f6a0..e14b25c 100644 --- a/src/frontend/components/nav.rs +++ b/src/frontend/components/nav.rs @@ -1,6 +1,5 @@ use crate::frontend::app::GlobalState; -use leptos::*; -use leptos::{component, use_context, view, IntoView, RwSignal, SignalWith}; +use leptos::{component, use_context, view, IntoView, RwSignal, SignalWith, *}; use leptos_router::*; #[component] diff --git a/src/frontend/markdown.rs b/src/frontend/markdown.rs index 1af830b..919c040 100644 --- a/src/frontend/markdown.rs +++ b/src/frontend/markdown.rs @@ -1,6 +1,11 @@ use crate::frontend::backend_hostname; -use markdown_it::parser::inline::{InlineRule, InlineState}; -use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; +use markdown_it::{ + parser::inline::{InlineRule, InlineState}, + MarkdownIt, + Node, + NodeValue, + Renderer, +}; pub fn markdown_parser() -> MarkdownIt { let mut parser = MarkdownIt::new(); diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index 45eae52..3f6ec6b 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -1,5 +1,4 @@ -use crate::common::utils::extract_domain; -use crate::common::{DbArticle, DbPerson}; +use crate::common::{utils::extract_domain, DbArticle, DbPerson}; use leptos::*; pub mod api; diff --git a/src/frontend/pages/article/actions.rs b/src/frontend/pages/article/actions.rs index 1dd7919..01fee0e 100644 --- a/src/frontend/pages/article/actions.rs +++ b/src/frontend/pages/article/actions.rs @@ -1,10 +1,14 @@ -use crate::common::ForkArticleData; -use crate::frontend::app::GlobalState; -use crate::frontend::article_link; -use crate::frontend::article_title; -use crate::frontend::components::article_nav::ArticleNav; -use crate::frontend::pages::article_resource; -use crate::frontend::DbArticle; +use crate::{ + common::ForkArticleForm, + frontend::{ + app::GlobalState, + article_link, + article_title, + components::article_nav::ArticleNav, + pages::article_resource, + DbArticle, + }, +}; use leptos::*; use leptos_router::Redirect; @@ -15,7 +19,7 @@ pub fn ArticleActions() -> impl IntoView { let (fork_response, set_fork_response) = create_signal(Option::::None); let (error, set_error) = create_signal(None::); let fork_action = create_action(move |(article_id, new_title): &(i32, String)| { - let params = ForkArticleData { + let params = ForkArticleForm { article_id: *article_id, new_title: new_title.to_string(), }; diff --git a/src/frontend/pages/article/create.rs b/src/frontend/pages/article/create.rs index 7f2e2ef..3457527 100644 --- a/src/frontend/pages/article/create.rs +++ b/src/frontend/pages/article/create.rs @@ -1,5 +1,4 @@ -use crate::common::CreateArticleData; -use crate::frontend::app::GlobalState; +use crate::{common::CreateArticleForm, frontend::app::GlobalState}; use leptos::*; use leptos_router::Redirect; @@ -18,7 +17,7 @@ pub fn CreateArticle() -> impl IntoView { let text = text.clone(); let summary = summary.clone(); async move { - let form = CreateArticleData { + let form = CreateArticleForm { title, text, summary, diff --git a/src/frontend/pages/article/edit.rs b/src/frontend/pages/article/edit.rs index 344a8d0..420beed 100644 --- a/src/frontend/pages/article/edit.rs +++ b/src/frontend/pages/article/edit.rs @@ -1,8 +1,12 @@ -use crate::common::{ApiConflict, ArticleView, EditArticleData}; -use crate::frontend::app::GlobalState; -use crate::frontend::article_title; -use crate::frontend::components::article_nav::ArticleNav; -use crate::frontend::pages::article_resource; +use crate::{ + common::{ApiConflict, ArticleView, EditArticleForm}, + frontend::{ + app::GlobalState, + article_title, + components::article_nav::ArticleNav, + pages::article_resource, + }, +}; use leptos::*; use leptos_router::use_params_map; @@ -65,7 +69,7 @@ pub fn EditArticle() -> impl IntoView { }; async move { set_edit_error.update(|e| *e = None); - let form = EditArticleData { + let form = EditArticleForm { article_id: article.article.id, new_text, summary, diff --git a/src/frontend/pages/article/history.rs b/src/frontend/pages/article/history.rs index 557a4f1..2a2c274 100644 --- a/src/frontend/pages/article/history.rs +++ b/src/frontend/pages/article/history.rs @@ -1,6 +1,9 @@ -use crate::frontend::components::article_nav::ArticleNav; -use crate::frontend::pages::article_resource; -use crate::frontend::{article_title, user_link}; +use crate::frontend::{ + article_title, + components::article_nav::ArticleNav, + pages::article_resource, + user_link, +}; use leptos::*; #[component] diff --git a/src/frontend/pages/article/list.rs b/src/frontend/pages/article/list.rs index 992bc94..0f0b58a 100644 --- a/src/frontend/pages/article/list.rs +++ b/src/frontend/pages/article/list.rs @@ -1,6 +1,7 @@ -use crate::common::ListArticlesData; -use crate::frontend::app::GlobalState; -use crate::frontend::{article_link, article_title}; +use crate::{ + common::ListArticlesForm, + frontend::{app::GlobalState, article_link, article_title}, +}; use leptos::*; use web_sys::wasm_bindgen::JsCast; @@ -11,7 +12,7 @@ pub fn ListArticles() -> impl IntoView { move || only_local.get(), |only_local| async move { GlobalState::api_client() - .list_articles(ListArticlesData { + .list_articles(ListArticlesForm { only_local: Some(only_local), }) .await diff --git a/src/frontend/pages/article/read.rs b/src/frontend/pages/article/read.rs index 53f47cf..98873af 100644 --- a/src/frontend/pages/article/read.rs +++ b/src/frontend/pages/article/read.rs @@ -1,7 +1,9 @@ -use crate::frontend::article_title; -use crate::frontend::components::article_nav::ArticleNav; -use crate::frontend::markdown::markdown_parser; -use crate::frontend::pages::article_resource; +use crate::frontend::{ + article_title, + components::article_nav::ArticleNav, + markdown::markdown_parser, + pages::article_resource, +}; use leptos::*; #[component] diff --git a/src/frontend/pages/conflicts.rs b/src/frontend/pages/conflicts.rs index b01bfb8..f162a1b 100644 --- a/src/frontend/pages/conflicts.rs +++ b/src/frontend/pages/conflicts.rs @@ -1,6 +1,4 @@ -use crate::frontend::app::GlobalState; -use crate::frontend::article_link; -use crate::frontend::article_title; +use crate::frontend::{app::GlobalState, article_link, article_title}; use leptos::*; #[component] diff --git a/src/frontend/pages/diff.rs b/src/frontend/pages/diff.rs index 0afdb48..6f0dcac 100644 --- a/src/frontend/pages/diff.rs +++ b/src/frontend/pages/diff.rs @@ -1,6 +1,4 @@ -use crate::frontend::components::article_nav::ArticleNav; -use crate::frontend::pages::article_resource; -use crate::frontend::user_link; +use crate::frontend::{components::article_nav::ArticleNav, pages::article_resource, user_link}; use leptos::*; use leptos_router::*; diff --git a/src/frontend/pages/instance_details.rs b/src/frontend/pages/instance_details.rs index ebacb11..2f5233e 100644 --- a/src/frontend/pages/instance_details.rs +++ b/src/frontend/pages/instance_details.rs @@ -1,6 +1,7 @@ -use crate::common::utils::http_protocol_str; -use crate::common::{DbInstance, FollowInstance}; -use crate::frontend::app::GlobalState; +use crate::{ + common::{utils::http_protocol_str, DbInstance, FollowInstance}, + frontend::app::GlobalState, +}; use leptos::*; use leptos_router::use_params_map; use url::Url; diff --git a/src/frontend/pages/login.rs b/src/frontend/pages/login.rs index c508f97..08385d3 100644 --- a/src/frontend/pages/login.rs +++ b/src/frontend/pages/login.rs @@ -1,6 +1,7 @@ -use crate::common::LoginUserData; -use crate::frontend::app::GlobalState; -use crate::frontend::components::credentials::*; +use crate::{ + common::LoginUserForm, + frontend::{app::GlobalState, components::credentials::*}, +}; use leptos::*; use leptos_router::Redirect; @@ -13,7 +14,7 @@ pub fn Login() -> impl IntoView { let login_action = create_action(move |(email, password): &(String, String)| { let username = email.to_string(); let password = password.to_string(); - let credentials = LoginUserData { username, password }; + let credentials = LoginUserForm { username, password }; async move { set_wait_for_response.update(|w| *w = true); let result = GlobalState::api_client().login(credentials).await; diff --git a/src/frontend/pages/mod.rs b/src/frontend/pages/mod.rs index ebcda15..d92ece4 100644 --- a/src/frontend/pages/mod.rs +++ b/src/frontend/pages/mod.rs @@ -1,5 +1,7 @@ -use crate::common::{ArticleView, GetArticleData, MAIN_PAGE_NAME}; -use crate::frontend::app::GlobalState; +use crate::{ + common::{ArticleView, GetArticleForm, MAIN_PAGE_NAME}, + frontend::app::GlobalState, +}; use leptos::{create_resource, Resource, SignalGet}; use leptos_router::use_params_map; @@ -23,7 +25,7 @@ fn article_resource() -> Resource, ArticleView> { domain = Some(domain_.to_string()); } GlobalState::api_client() - .get_article(GetArticleData { + .get_article(GetArticleForm { title: Some(title), domain, id: None, diff --git a/src/frontend/pages/register.rs b/src/frontend/pages/register.rs index ef025df..9735814 100644 --- a/src/frontend/pages/register.rs +++ b/src/frontend/pages/register.rs @@ -1,7 +1,7 @@ -use crate::common::{LocalUserView, RegisterUserData}; -use crate::frontend::app::GlobalState; -use crate::frontend::components::credentials::*; -use crate::frontend::error::MyResult; +use crate::{ + common::{LocalUserView, RegisterUserForm}, + frontend::{app::GlobalState, components::credentials::*, error::MyResult}, +}; use leptos::{logging::log, *}; #[component] @@ -13,7 +13,7 @@ pub fn Register() -> impl IntoView { let register_action = create_action(move |(email, password): &(String, String)| { let username = email.to_string(); let password = password.to_string(); - let credentials = RegisterUserData { username, password }; + let credentials = RegisterUserForm { username, password }; log!("Try to register new account for {}", credentials.username); async move { set_wait_for_response.update(|w| *w = true); diff --git a/src/frontend/pages/search.rs b/src/frontend/pages/search.rs index 786bed3..a470ae9 100644 --- a/src/frontend/pages/search.rs +++ b/src/frontend/pages/search.rs @@ -1,6 +1,7 @@ -use crate::common::{DbArticle, DbInstance, SearchArticleData}; -use crate::frontend::app::GlobalState; -use crate::frontend::{article_link, article_title}; +use crate::{ + common::{DbArticle, DbInstance, SearchArticleForm}, + frontend::{app::GlobalState, article_link, article_title}, +}; use leptos::*; use leptos_router::use_query_map; use serde::{Deserialize, Serialize}; @@ -28,7 +29,7 @@ pub fn Search() -> impl IntoView { let mut search_results = SearchResults::default(); let api_client = GlobalState::api_client(); let url = Url::parse(&query); - let search_data = SearchArticleData { query }; + let search_data = SearchArticleForm { query }; let search = api_client.search(&search_data); match search.await { diff --git a/src/frontend/pages/user_profile.rs b/src/frontend/pages/user_profile.rs index 89b6806..2e940da 100644 --- a/src/frontend/pages/user_profile.rs +++ b/src/frontend/pages/user_profile.rs @@ -1,6 +1,7 @@ -use crate::common::{DbPerson, GetUserData}; -use crate::frontend::app::GlobalState; -use crate::frontend::user_title; +use crate::{ + common::{DbPerson, GetUserForm}, + frontend::{app::GlobalState, user_title}, +}; use leptos::*; use leptos_router::use_params_map; @@ -16,7 +17,7 @@ pub fn UserProfile() -> impl IntoView { name = title_.to_string(); domain = Some(domain_.to_string()); } - let params = GetUserData { name, domain }; + let params = GetUserForm { name, domain }; GlobalState::api_client().get_user(params).await.unwrap() }); diff --git a/src/main.rs b/src/main.rs index f7da268..f5d0e4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,8 +23,7 @@ pub async fn main() -> ibis_lib::backend::error::MyResult<()> { #[cfg(not(feature = "ssr"))] fn main() { use ibis_lib::frontend::app::App; - use leptos::mount_to_body; - use leptos::view; + use leptos::{mount_to_body, view}; _ = console_log::init_with_level(log::Level::Debug); console_error_panic_hook::set_once(); diff --git a/tests/common.rs b/tests/common.rs index 2598169..52bdf03 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,19 +1,26 @@ #![allow(clippy::unwrap_used)] -use ibis_lib::backend::config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation}; -use ibis_lib::backend::start; -use ibis_lib::common::RegisterUserData; -use ibis_lib::frontend::api::ApiClient; -use ibis_lib::frontend::error::MyResult; +use ibis_lib::{ + backend::{ + config::{IbisConfig, IbisConfigDatabase, IbisConfigFederation}, + start, + }, + common::RegisterUserForm, + frontend::{api::ApiClient, error::MyResult}, +}; use reqwest::ClientBuilder; -use std::env::current_dir; -use std::fs::{create_dir_all, remove_dir_all}; -use std::ops::Deref; -use std::process::{Command, Stdio}; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::Once; -use std::thread::{sleep, spawn}; -use std::time::Duration; +use std::{ + env::current_dir, + fs::{create_dir_all, remove_dir_all}, + ops::Deref, + process::{Command, Stdio}, + sync::{ + atomic::{AtomicI32, Ordering}, + Once, + }, + thread::{sleep, spawn}, + time::Duration, +}; use tokio::task::JoinHandle; use tracing::log::LevelFilter; @@ -128,7 +135,7 @@ impl IbisInstance { }); // wait a moment for the backend to start tokio::time::sleep(Duration::from_millis(5000)).await; - let form = RegisterUserData { + let form = RegisterUserForm { username: username.to_string(), password: "hunter2".to_string(), }; diff --git a/tests/test.rs b/tests/test.rs index b8fef89..6be8412 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -5,13 +5,23 @@ extern crate ibis_lib; mod common; use crate::common::{TestData, TEST_ARTICLE_DEFAULT_TEXT}; -use ibis_lib::common::utils::extract_domain; -use ibis_lib::common::{ - ArticleView, EditArticleData, ForkArticleData, GetArticleData, GetUserData, ListArticlesData, +use ibis_lib::{ + common::{ + utils::extract_domain, + ArticleView, + CreateArticleForm, + EditArticleForm, + ForkArticleForm, + GetArticleForm, + GetUserForm, + ListArticlesForm, + LoginUserForm, + ProtectArticleForm, + RegisterUserForm, + SearchArticleForm, + }, + frontend::error::MyResult, }; -use ibis_lib::common::{CreateArticleData, SearchArticleData}; -use ibis_lib::common::{LoginUserData, RegisterUserData}; -use ibis_lib::frontend::error::MyResult; use pretty_assertions::{assert_eq, assert_ne}; use url::Url; @@ -20,7 +30,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> { let data = TestData::start().await; // create article - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -30,7 +40,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> { assert!(create_res.article.local); // now article can be read - let get_article_data = GetArticleData { + let get_article_data = GetArticleForm { title: Some(create_res.article.title.clone()), domain: None, id: None, @@ -45,7 +55,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> { assert!(not_found.is_err()); // edit article - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: create_res.article.id, new_text: "Lorem Ipsum 2\n".to_string(), summary: "summary".to_string(), @@ -57,7 +67,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> { assert_eq!(2, edit_res.edits.len()); assert_eq!(edit_form.summary, edit_res.edits[1].edit.summary); - let search_form = SearchArticleData { + let search_form = SearchArticleForm { query: create_form.title.clone(), }; let search_res = data.alpha.search(&search_form).await?; @@ -66,7 +76,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> { let list_articles = data .alpha - .list_articles(ListArticlesData { + .list_articles(ListArticlesForm { only_local: Some(false), }) .await?; @@ -81,7 +91,7 @@ async fn test_create_duplicate_article() -> MyResult<()> { let data = TestData::start().await; // create article - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -127,7 +137,7 @@ async fn test_synchronize_articles() -> MyResult<()> { let data = TestData::start().await; // create article on alpha - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -138,7 +148,7 @@ async fn test_synchronize_articles() -> MyResult<()> { assert!(create_res.article.local); // edit the article - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: create_res.article.id, new_text: "Lorem Ipsum 2\n".to_string(), summary: "summary".to_string(), @@ -153,7 +163,7 @@ async fn test_synchronize_articles() -> MyResult<()> { .resolve_instance(Url::parse(&format!("http://{}", &data.alpha.hostname))?) .await?; - let mut get_article_data = GetArticleData { + let mut get_article_data = GetArticleForm { title: Some(create_res.article.title), domain: None, id: None, @@ -185,7 +195,7 @@ async fn test_edit_local_article() -> MyResult<()> { .await?; // create new article - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -195,7 +205,7 @@ async fn test_edit_local_article() -> MyResult<()> { assert!(create_res.article.local); // article should be federated to alpha - let get_article_data = GetArticleData { + let get_article_data = GetArticleForm { title: Some(create_res.article.title.to_string()), domain: Some(beta_instance.domain), id: None, @@ -207,7 +217,7 @@ async fn test_edit_local_article() -> MyResult<()> { assert_eq!(create_res.article.text, get_res.article.text); // edit the article - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: create_res.article.id, new_text: "Lorem Ipsum 2\n".to_string(), summary: "summary".to_string(), @@ -246,7 +256,7 @@ async fn test_edit_remote_article() -> MyResult<()> { .await?; // create new article - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -256,7 +266,7 @@ async fn test_edit_remote_article() -> MyResult<()> { assert!(create_res.article.local); // article should be federated to alpha and gamma - let get_article_data_alpha = GetArticleData { + let get_article_data_alpha = GetArticleForm { title: Some(create_res.article.title.to_string()), domain: Some(beta_id_on_alpha.domain), id: None, @@ -269,7 +279,7 @@ async fn test_edit_remote_article() -> MyResult<()> { assert_eq!(1, get_res.edits.len()); assert!(!get_res.article.local); - let get_article_data_gamma = GetArticleData { + let get_article_data_gamma = GetArticleForm { title: Some(create_res.article.title.to_string()), domain: Some(beta_id_on_gamma.domain), id: None, @@ -281,7 +291,7 @@ async fn test_edit_remote_article() -> MyResult<()> { assert_eq!(create_res.article.title, get_res.article.title); assert_eq!(create_res.article.text, get_res.article.text); - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: get_res.article.id, new_text: "Lorem Ipsum 2\n".to_string(), summary: "summary".to_string(), @@ -317,7 +327,7 @@ async fn test_local_edit_conflict() -> MyResult<()> { let data = TestData::start().await; // create new article - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -327,7 +337,7 @@ async fn test_local_edit_conflict() -> MyResult<()> { assert!(create_res.article.local); // one user edits article - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: create_res.article.id, new_text: "Lorem Ipsum\n".to_string(), summary: "summary".to_string(), @@ -339,7 +349,7 @@ async fn test_local_edit_conflict() -> MyResult<()> { assert_eq!(2, edit_res.edits.len()); // another user edits article, without being aware of previous edit - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: create_res.article.id, new_text: "Ipsum Lorem\n".to_string(), summary: "summary".to_string(), @@ -357,7 +367,7 @@ async fn test_local_edit_conflict() -> MyResult<()> { assert_eq!(1, conflicts.len()); assert_eq!(conflicts[0], edit_res); - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: create_res.article.id, new_text: "Lorem Ipsum and Ipsum Lorem\n".to_string(), summary: "summary".to_string(), @@ -383,7 +393,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { .await?; // create new article - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -400,7 +410,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { assert_eq!(create_res.article.text, resolve_res.article.text); // alpha edits article - let get_article_data = GetArticleData { + let get_article_data = GetArticleForm { title: Some(create_form.title.to_string()), domain: Some(beta_id_on_alpha.domain), id: None, @@ -408,7 +418,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { let get_res = data.alpha.get_article(get_article_data).await?; assert_eq!(&create_res.edits.len(), &get_res.edits.len()); assert_eq!(&create_res.edits[0].edit.hash, &get_res.edits[0].edit.hash); - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: get_res.article.id, new_text: "Lorem Ipsum\n".to_string(), summary: "summary".to_string(), @@ -427,7 +437,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { // gamma also edits, as its not the latest version there is a conflict. local version should // not be updated with this conflicting version, instead user needs to handle the conflict - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: resolve_res.article.id, new_text: "aaaa\n".to_string(), summary: "summary".to_string(), @@ -443,7 +453,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { assert_eq!(1, conflicts.len()); // resolve the conflict - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: resolve_res.article.id, new_text: "aaaa\n".to_string(), summary: "summary".to_string(), @@ -465,7 +475,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> { let data = TestData::start().await; // create new article - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -475,7 +485,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> { assert!(create_res.article.local); // one user edits article - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: create_res.article.id, new_text: "my\nexample\ntext\n".to_string(), summary: "summary".to_string(), @@ -487,7 +497,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> { assert_eq!(2, edit_res.edits.len()); // another user edits article, without being aware of previous edit - let edit_form = EditArticleData { + let edit_form = EditArticleForm { article_id: create_res.article.id, new_text: "some\nexample\narticle\n".to_string(), summary: "summary".to_string(), @@ -508,7 +518,7 @@ async fn test_fork_article() -> MyResult<()> { let data = TestData::start().await; // create article - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -526,7 +536,7 @@ async fn test_fork_article() -> MyResult<()> { assert_eq!(create_res.edits.len(), resolve_res.edits.len()); // fork the article to local instance - let fork_form = ForkArticleData { + let fork_form = ForkArticleForm { article_id: resolved_article.id, new_title: resolved_article.title.clone(), }; @@ -546,7 +556,7 @@ async fn test_fork_article() -> MyResult<()> { assert_eq!(forked_article.instance_id, beta_instance.instance.id); // now search returns two articles for this title (original and forked) - let search_form = SearchArticleData { + let search_form = SearchArticleForm { query: create_form.title.clone(), }; let search_res = data.beta.search(&search_form).await?; @@ -560,20 +570,20 @@ async fn test_user_registration_login() -> MyResult<()> { let data = TestData::start().await; let username = "my_user"; let password = "hunter2"; - let register_data = RegisterUserData { + let register_data = RegisterUserForm { username: username.to_string(), password: password.to_string(), }; data.alpha.register(register_data).await?; - let login_data = LoginUserData { + let login_data = LoginUserForm { username: username.to_string(), password: "asd123".to_string(), }; let invalid_login = data.alpha.login(login_data).await; assert!(invalid_login.is_err()); - let login_data = LoginUserData { + let login_data = LoginUserForm { username: username.to_string(), password: password.to_string(), }; @@ -595,7 +605,7 @@ async fn test_user_profile() -> MyResult<()> { let data = TestData::start().await; // Create an article and federate it, in order to federate the user who created it - let create_form = CreateArticleData { + let create_form = CreateArticleForm { title: "Manu_Chao".to_string(), text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), summary: "create article".to_string(), @@ -607,7 +617,7 @@ async fn test_user_profile() -> MyResult<()> { let domain = extract_domain(&data.alpha.my_profile().await?.person.ap_id); // Now we can fetch the remote user from local api - let params = GetUserData { + let params = GetUserForm { name: "alpha".to_string(), domain: Some(domain), }; @@ -617,3 +627,50 @@ async fn test_user_profile() -> MyResult<()> { data.stop() } + +#[tokio::test] +async fn test_lock_article() -> MyResult<()> { + let data = TestData::start().await; + + // create article + let create_form = CreateArticleForm { + title: "Manu_Chao".to_string(), + text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), + summary: "create article".to_string(), + }; + let create_res = data.alpha.create_article(&create_form).await?; + assert!(!create_res.article.protected); + + // lock from normal user fails + let lock_form = ProtectArticleForm { + article_id: create_res.article.id, + protected: true, + }; + let lock_res = data.alpha.protect_article(&lock_form).await; + assert!(lock_res.is_err()); + + // login as admin to lock article + let form = LoginUserForm { + username: "ibis".to_string(), + password: "ibis".to_string(), + }; + data.alpha.login(form).await?; + let lock_res = data.alpha.protect_article(&lock_form).await?; + assert!(lock_res.protected); + + let resolve_res: ArticleView = data + .gamma + .resolve_article(create_res.article.ap_id.inner().clone()) + .await?; + let edit_form = EditArticleForm { + article_id: resolve_res.article.id, + new_text: "test".to_string(), + summary: "test".to_string(), + previous_version_id: resolve_res.latest_version, + resolve_conflict_id: None, + }; + let edit_res = data.gamma.edit_article(&edit_form).await; + assert!(edit_res.is_err()); + + data.stop() +}