diff --git a/src/api.rs b/src/api/article.rs similarity index 53% rename from src/api.rs rename to src/api/article.rs index d576b60..b0d70e8 100644 --- a/src/api.rs +++ b/src/api/article.rs @@ -1,46 +1,21 @@ use crate::database::article::{ArticleView, DbArticle, DbArticleForm}; use crate::database::conflict::{ApiConflict, DbConflict, DbConflictForm}; use crate::database::edit::{DbEdit, DbEditForm}; -use crate::database::instance::{DbInstance, InstanceView}; -use crate::database::user::{DbLocalUser, DbPerson}; +use crate::database::instance::DbInstance; use crate::database::version::EditVersion; use crate::database::MyDataHandle; use crate::error::MyResult; use crate::federation::activities::create_article::CreateArticle; -use crate::federation::activities::follow::Follow; use crate::federation::activities::submit_article_update; use crate::utils::generate_article_version; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; -use anyhow::anyhow; use axum::extract::Query; -use axum::routing::{get, post}; -use axum::{Form, Json, Router}; +use axum::Form; +use axum::Json; use axum_macros::debug_handler; -use bcrypt::verify; -use chrono::Utc; use diffy::create_patch; -use futures::future::try_join_all; -use jsonwebtoken::{encode, EncodingKey, Header}; use serde::{Deserialize, Serialize}; -use url::Url; - -pub fn api_routes() -> Router { - Router::new() - .route( - "/article", - get(get_article).post(create_article).patch(edit_article), - ) - .route("/article/fork", post(fork_article)) - .route("/edit_conflicts", get(edit_conflicts)) - .route("/resolve_instance", get(resolve_instance)) - .route("/resolve_article", get(resolve_article)) - .route("/instance", get(get_local_instance)) - .route("/instance/follow", post(follow_instance)) - .route("/search", get(search_article)) - .route("/user/register", post(register_user)) - .route("/user/login", post(login_user)) -} #[derive(Deserialize, Serialize)] pub struct CreateArticleData { @@ -49,7 +24,7 @@ pub struct CreateArticleData { /// Create a new article with empty text, and federate it to followers. #[debug_handler] -async fn create_article( +pub(in crate::api) async fn create_article( data: Data, Form(create_article): Form, ) -> MyResult> { @@ -98,7 +73,7 @@ pub struct EditArticleData { /// /// Conflicts are stored in the database so they can be retrieved later from `/api/v3/edit_conflicts`. #[debug_handler] -async fn edit_article( +pub(in crate::api) async fn edit_article( data: Data, Form(edit_form): Form, ) -> MyResult>> { @@ -144,7 +119,7 @@ pub struct GetArticleData { /// Retrieve an article by ID. It must already be stored in the local database. #[debug_handler] -async fn get_article( +pub(in crate::api) async fn get_article( Query(query): Query, data: Data, ) -> MyResult> { @@ -154,97 +129,6 @@ async fn get_article( )?)) } -#[derive(Deserialize, Serialize)] -pub struct ResolveObject { - pub id: Url, -} - -/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to -/// the local instance, and allows for interactions such as following. -#[debug_handler] -async fn resolve_instance( - Query(query): Query, - data: Data, -) -> MyResult> { - let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?; - Ok(Json(instance)) -} - -/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new -/// article changes can only be received if we follow the instance, or if it is refetched manually. -#[debug_handler] -async fn resolve_article( - Query(query): Query, - data: Data, -) -> MyResult> { - let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?; - let edits = DbEdit::read_for_article(&article, &data.db_connection)?; - let latest_version = edits.last().unwrap().hash.clone(); - Ok(Json(ArticleView { - article, - edits, - latest_version, - })) -} - -/// Retrieve the local instance info. -#[debug_handler] -async fn get_local_instance(data: Data) -> MyResult> { - let local_instance = DbInstance::read_local_view(&data.db_connection)?; - Ok(Json(local_instance)) -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct FollowInstance { - pub id: i32, -} - -/// Make the local instance follow a given remote instance, to receive activities about new and -/// updated articles. -#[debug_handler] -async fn follow_instance( - data: Data, - Form(query): Form, -) -> MyResult<()> { - let local_instance = DbInstance::read_local_instance(&data.db_connection)?; - let target = DbInstance::read(query.id, &data.db_connection)?; - let pending = !target.local; - DbInstance::follow(local_instance.id, target.id, pending, &data)?; - let instance = DbInstance::read(query.id, &data.db_connection)?; - Follow::send(local_instance, instance, &data).await?; - Ok(()) -} - -/// Get a list of all unresolved edit conflicts. -#[debug_handler] -async fn edit_conflicts(data: Data) -> MyResult>> { - let conflicts = DbConflict::list(&data.db_connection)?; - let conflicts: Vec = try_join_all(conflicts.into_iter().map(|c| { - let data = data.reset_request_count(); - async move { c.to_api_conflict(&data).await } - })) - .await? - .into_iter() - .flatten() - .collect(); - Ok(Json(conflicts)) -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct SearchArticleData { - pub query: String, -} - -/// Search articles for matching title or body text. -#[debug_handler] -async fn search_article( - Query(query): Query, - data: Data, -) -> MyResult>> { - let article = DbArticle::search(&query.query, &data.db_connection)?; - Ok(Json(article)) -} - #[derive(Deserialize, Serialize)] pub struct ForkArticleData { // TODO: could add optional param new_title so there is no problem with title collision @@ -256,7 +140,7 @@ pub struct ForkArticleData { /// Fork a remote article to local instance. This is useful if there are disagreements about /// how an article should be edited. #[debug_handler] -async fn fork_article( +pub(in crate::api) async fn fork_article( data: Data, Form(fork_form): Form, ) -> MyResult> { @@ -298,69 +182,3 @@ async fn fork_article( Ok(Json(DbArticle::read_view(article.id, &data.db_connection)?)) } - -#[derive(Debug, Serialize, Deserialize)] -pub struct Claims { - /// local_user.id - pub sub: String, - /// hostname - pub iss: String, - /// Creation time as unix timestamp - pub iat: i64, -} - -pub fn generate_login_token( - local_user: DbLocalUser, - data: &Data, -) -> MyResult { - let hostname = data.domain().to_string(); - let claims = Claims { - sub: local_user.id.to_string(), - iss: hostname, - iat: Utc::now().timestamp(), - }; - - // TODO: move to config - let key = EncodingKey::from_secret("secret".as_bytes()); - let jwt = encode(&Header::default(), &claims, &key)?; - Ok(LoginResponse { jwt }) -} - -#[derive(Deserialize, Serialize)] -pub struct RegisterUserData { - pub name: String, - pub password: String, -} - -#[derive(Deserialize, Serialize)] -pub struct LoginResponse { - pub jwt: String, -} - -#[debug_handler] -async fn register_user( - data: Data, - Form(form): Form, -) -> MyResult> { - let user = DbPerson::create_local(form.name, form.password, &data)?; - Ok(Json(generate_login_token(user.local_user, &data)?)) -} - -#[derive(Deserialize, Serialize)] -pub struct LoginUserData { - name: String, - password: String, -} - -#[debug_handler] -async fn login_user( - data: Data, - Form(form): Form, -) -> MyResult> { - let user = DbPerson::read_local_from_name(&form.name, &data)?; - let valid = verify(&form.password, &user.local_user.password_encrypted)?; - if !valid { - return Err(anyhow!("Invalid login").into()); - } - Ok(Json(generate_login_token(user.local_user, &data)?)) -} diff --git a/src/api/instance.rs b/src/api/instance.rs new file mode 100644 index 0000000..dc6eaf9 --- /dev/null +++ b/src/api/instance.rs @@ -0,0 +1,38 @@ +use crate::database::instance::{DbInstance, InstanceView}; +use crate::database::MyDataHandle; +use crate::error::MyResult; +use crate::federation::activities::follow::Follow; +use activitypub_federation::config::Data; +use axum::{Form, Json}; +use axum_macros::debug_handler; +use serde::{Deserialize, Serialize}; + +/// Retrieve the local instance info. +#[debug_handler] +pub(in crate::api) async fn get_local_instance( + data: Data, +) -> MyResult> { + let local_instance = DbInstance::read_local_view(&data.db_connection)?; + Ok(Json(local_instance)) +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct FollowInstance { + pub id: i32, +} + +/// Make the local instance follow a given remote instance, to receive activities about new and +/// updated articles. +#[debug_handler] +pub(in crate::api) async fn follow_instance( + data: Data, + Form(query): Form, +) -> MyResult<()> { + let local_instance = DbInstance::read_local_instance(&data.db_connection)?; + let target = DbInstance::read(query.id, &data.db_connection)?; + let pending = !target.local; + DbInstance::follow(local_instance.id, target.id, pending, &data)?; + let instance = DbInstance::read(query.id, &data.db_connection)?; + Follow::send(local_instance, instance, &data).await?; + Ok(()) +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..8473b1a --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,105 @@ +use crate::api::article::create_article; +use crate::api::article::{edit_article, fork_article, get_article}; +use crate::api::instance::follow_instance; +use crate::api::instance::get_local_instance; +use crate::api::user::login_user; +use crate::api::user::register_user; +use crate::database::article::{ArticleView, DbArticle}; +use crate::database::conflict::{ApiConflict, DbConflict}; +use crate::database::edit::DbEdit; +use crate::database::instance::DbInstance; +use crate::database::MyDataHandle; +use crate::error::MyResult; +use activitypub_federation::config::Data; +use activitypub_federation::fetch::object_id::ObjectId; +use axum::extract::Query; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use axum_macros::debug_handler; +use futures::future::try_join_all; +use serde::{Deserialize, Serialize}; +use url::Url; + +pub mod article; +pub mod instance; +pub mod user; + +pub fn api_routes() -> Router { + Router::new() + .route( + "/article", + get(get_article).post(create_article).patch(edit_article), + ) + .route("/article/fork", post(fork_article)) + .route("/edit_conflicts", get(edit_conflicts)) + .route("/resolve_instance", get(resolve_instance)) + .route("/resolve_article", get(resolve_article)) + .route("/instance", get(get_local_instance)) + .route("/instance/follow", post(follow_instance)) + .route("/search", get(search_article)) + .route("/user/register", post(register_user)) + .route("/user/login", post(login_user)) +} + +#[derive(Deserialize, Serialize)] +pub struct ResolveObject { + pub id: Url, +} + +/// Fetch a remote instance actor. This automatically synchronizes the remote articles collection to +/// the local instance, and allows for interactions such as following. +#[debug_handler] +async fn resolve_instance( + Query(query): Query, + data: Data, +) -> MyResult> { + let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?; + Ok(Json(instance)) +} + +/// Fetch a remote article, including edits collection. Allows viewing and editing. Note that new +/// article changes can only be received if we follow the instance, or if it is refetched manually. +#[debug_handler] +async fn resolve_article( + Query(query): Query, + data: Data, +) -> MyResult> { + let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?; + let edits = DbEdit::read_for_article(&article, &data.db_connection)?; + let latest_version = edits.last().unwrap().hash.clone(); + Ok(Json(ArticleView { + article, + edits, + latest_version, + })) +} + +/// Get a list of all unresolved edit conflicts. +#[debug_handler] +async fn edit_conflicts(data: Data) -> MyResult>> { + let conflicts = DbConflict::list(&data.db_connection)?; + let conflicts: Vec = try_join_all(conflicts.into_iter().map(|c| { + let data = data.reset_request_count(); + async move { c.to_api_conflict(&data).await } + })) + .await? + .into_iter() + .flatten() + .collect(); + Ok(Json(conflicts)) +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct SearchArticleData { + pub query: String, +} + +/// Search articles for matching title or body text. +#[debug_handler] +async fn search_article( + Query(query): Query, + data: Data, +) -> MyResult>> { + let article = DbArticle::search(&query.query, &data.db_connection)?; + Ok(Json(article)) +} diff --git a/src/api/user.rs b/src/api/user.rs new file mode 100644 index 0000000..ca76716 --- /dev/null +++ b/src/api/user.rs @@ -0,0 +1,77 @@ +use crate::database::user::{DbLocalUser, DbPerson}; +use crate::database::MyDataHandle; +use crate::error::MyResult; +use activitypub_federation::config::Data; +use anyhow::anyhow; +use axum::{Form, Json}; +use axum_macros::debug_handler; +use bcrypt::verify; +use chrono::Utc; +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + /// local_user.id + pub sub: String, + /// hostname + pub iss: String, + /// Creation time as unix timestamp + pub iat: i64, +} + +pub(in crate::api) fn generate_login_token( + local_user: DbLocalUser, + data: &Data, +) -> MyResult { + let hostname = data.domain().to_string(); + let claims = Claims { + sub: local_user.id.to_string(), + iss: hostname, + iat: Utc::now().timestamp(), + }; + + // TODO: move to config + let key = EncodingKey::from_secret("secret".as_bytes()); + let jwt = encode(&Header::default(), &claims, &key)?; + Ok(LoginResponse { jwt }) +} + +#[derive(Deserialize, Serialize)] +pub struct RegisterUserData { + pub name: String, + pub password: String, +} + +#[derive(Deserialize, Serialize)] +pub struct LoginResponse { + pub jwt: String, +} + +#[debug_handler] +pub(in crate::api) async fn register_user( + data: Data, + Form(form): Form, +) -> MyResult> { + let user = DbPerson::create_local(form.name, form.password, &data)?; + Ok(Json(generate_login_token(user.local_user, &data)?)) +} + +#[derive(Deserialize, Serialize)] +pub struct LoginUserData { + name: String, + password: String, +} + +#[debug_handler] +pub(in crate::api) async fn login_user( + data: Data, + Form(form): Form, +) -> MyResult> { + let user = DbPerson::read_local_from_name(&form.name, &data)?; + let valid = verify(&form.password, &user.local_user.password_encrypted)?; + if !valid { + return Err(anyhow!("Invalid login").into()); + } + Ok(Json(generate_login_token(user.local_user, &data)?)) +} diff --git a/src/lib.rs b/src/lib.rs index bf4d242..ecf2299 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -use crate::api::api_routes; use crate::database::instance::{DbInstance, DbInstanceForm}; use crate::database::MyData; use crate::error::MyResult; @@ -8,6 +7,7 @@ use activitypub_federation::config::{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 api::api_routes; use axum::{Router, Server}; use chrono::Local; use diesel::Connection; diff --git a/tests/common.rs b/tests/common.rs index 27c2163..305df5b 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; -use fediwiki::api::{ - CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject, -}; +use fediwiki::api::article::{CreateArticleData, EditArticleData, GetArticleData}; +use fediwiki::api::instance::FollowInstance; +use fediwiki::api::ResolveObject; use fediwiki::database::article::ArticleView; use fediwiki::database::conflict::ApiConflict; use fediwiki::database::instance::DbInstance; @@ -20,7 +20,6 @@ use std::thread::{sleep, spawn}; use std::time::Duration; use tokio::task::JoinHandle; use tracing::log::LevelFilter; -use tracing::warn; use url::Url; pub static CLIENT: Lazy = Lazy::new(Client::new); diff --git a/tests/test.rs b/tests/test.rs index f22efc8..d767c10 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -7,10 +7,9 @@ use crate::common::{ get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT, }; use common::get; -use fediwiki::api::{ - EditArticleData, ForkArticleData, LoginResponse, RegisterUserData, ResolveObject, - SearchArticleData, -}; +use fediwiki::api::article::{EditArticleData, ForkArticleData}; +use fediwiki::api::user::{LoginResponse, RegisterUserData}; +use fediwiki::api::{ResolveObject, SearchArticleData}; use fediwiki::database::article::{ArticleView, DbArticle}; use fediwiki::database::conflict::ApiConflict; use fediwiki::database::instance::{DbInstance, InstanceView};