diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/entity/user.rs | 64 | ||||
-rw-r--r-- | src/main.rs | 48 | ||||
-rw-r--r-- | src/server.rs | 14 | ||||
-rw-r--r-- | src/server/routes.rs | 14 | ||||
-rw-r--r-- | src/server/routes/users.rs | 4 | ||||
-rw-r--r-- | src/server/routes/users/get_outbox.rs | 77 | ||||
-rw-r--r-- | src/server/routes/users/get_user.rs | 49 | ||||
-rw-r--r-- | src/server/routes/users/webfinger.rs | 23 | ||||
-rw-r--r-- | src/state.rs | 17 |
9 files changed, 259 insertions, 51 deletions
diff --git a/src/entity/user.rs b/src/entity/user.rs index e136cb3..da22f00 100644 --- a/src/entity/user.rs +++ b/src/entity/user.rs @@ -8,36 +8,77 @@ use activitypub_federation::{ }; use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use stack_up::Services; use tracing::trace; use url::Url; +use uuid::Uuid; use crate::{error::AppError, state::AppHandle}; #[derive(PartialEq, Clone, Debug)] pub(crate) struct User { + pub id: String, pub username: String, pub ap_id: ObjectId<User>, pub private_key: Option<String>, pub public_key: String, pub inbox: Url, + pub outbox: Option<Url>, } -impl User { - pub fn new(username: &str) -> Result<Self, AppError> { - trace!("creating a new user"); - let keys = generate_actor_keypair()?; - let stub = &format!("http://localhost/users/{username}"); +pub struct DbUser { + pub id: String, + pub username: String, + pub ap_id: String, + pub private_key: Option<String>, + pub public_key: String, + pub inbox: String, + pub outbox: Option<String>, + pub local: bool, +} +impl TryFrom<DbUser> for User { + type Error = AppError; + fn try_from(value: DbUser) -> Result<Self, Self::Error> { Ok(Self { - username: username.to_owned(), - ap_id: Url::parse(stub)?.into(), - private_key: Some(keys.private_key), - public_key: keys.public_key, - inbox: Url::parse(&format!("{stub}/inbox"))?, + id: value.id, + username: value.username, + ap_id: Url::parse(&value.ap_id)?.into(), + private_key: value.private_key, + public_key: value.public_key, + inbox: Url::parse(&value.inbox)?, + outbox: match value.outbox { + Some(ref url) => Some(Url::parse(url)?), + None => None, + }, }) } } +impl User { + pub async fn new(username: &str, services: &Services) -> Result<Self, AppError> { + trace!("creating keypair for new user"); + let keys = generate_actor_keypair()?; + let stub = &format!("http://localhost/users/{username}"); + let id = Uuid::now_v7(); + + trace!(id = ?id, "creating a new user"); + let user = sqlx::query_as!( + DbUser, + "insert into account (id, username, ap_id, private_key, public_key, inbox, outbox, local) values ($1, $2, $3, $4, $5, $6, $7, $8) returning *", + id, + username, + stub, + keys.private_key, + keys.public_key, + &format!("{stub}/inbox"), + &format!("{stub}/outbox"), + true + ).fetch_one(&services.postgres).await?; + Self::try_from(user) + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Person { @@ -47,6 +88,8 @@ pub struct Person { id: ObjectId<User>, inbox: Url, public_key: PublicKey, + #[serde(skip_serializing_if = "Option::is_none")] + outbox: Option<Url>, } #[async_trait] @@ -85,6 +128,7 @@ impl Object for User { id: self.ap_id.clone(), inbox: self.inbox.clone(), public_key: self.public_key(), + outbox: self.outbox.clone(), }) } diff --git a/src/main.rs b/src/main.rs index 1e5b259..68aedc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,19 +3,53 @@ mod error; mod server; mod state; -use stack_up::{Monitoring, tracing::Tracing}; +use std::net::{Ipv6Addr, SocketAddr}; + +use clap::Parser; +use stack_up::{Configuration, Services, tracing::Tracing}; use crate::{error::AppError, state::AppState}; +use tracing::{error, info}; + +/// sellershut +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Path to config file + #[arg(short, long)] + config_file: Option<std::path::PathBuf>, +} #[tokio::main] async fn main() -> Result<(), AppError> { - let _tracing = Tracing::builder().build(&Monitoring { - log_level: "trace".into(), - }); + let args = Args::parse(); + let config = include_str!("../sellershut.toml"); + + let mut config = config::Config::builder() + .add_source(config::File::from_str(config, config::FileFormat::Toml)); + + if let Some(cf) = args.config_file.as_ref().and_then(|v| v.to_str()) { + config = config.add_source(config::File::new(cf, config::FileFormat::Toml)); + }; + + let mut config: Configuration = config.build()?.try_deserialize()?; + config.application.name = env!("CARGO_CRATE_NAME").into(); + config.application.version = env!("CARGO_PKG_VERSION").into(); + + let _tracing = Tracing::builder().build(&config.monitoring); + + let services = Services::builder() + .postgres(&config.database) + .await + .inspect_err(|e| error!("database: {e}"))? + .build(); + + let state = AppState::new(services, &config).await?; + + let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, config.application.port)); - let state = AppState::new().await?; - let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?; - tracing::debug!("listening on {}", listener.local_addr()?); + let listener = tokio::net::TcpListener::bind(addr).await?; + info!(port = addr.port(), "serving api"); axum::serve(listener, server::router(state)).await?; Ok(()) diff --git a/src/server.rs b/src/server.rs index bb2b4d0..85a3e81 100644 --- a/src/server.rs +++ b/src/server.rs @@ -13,3 +13,17 @@ pub fn router(state: FederationConfig<AppHandle>) -> Router { .layer(TraceLayer::new_for_http()) .layer(FederationMiddleware::new(state)) } + +#[cfg(test)] +pub(crate) fn test_config() -> stack_up::Configuration { + use stack_up::Configuration; + + let config_path = "sellershut.toml"; + + let config = config::Config::builder() + .add_source(config::File::new(config_path, config::FileFormat::Toml)) + .build() + .unwrap(); + + config.try_deserialize::<Configuration>().unwrap() +} diff --git a/src/server/routes.rs b/src/server/routes.rs index 7751c95..85cc100 100644 --- a/src/server/routes.rs +++ b/src/server/routes.rs @@ -14,13 +14,19 @@ mod tests { body::Body, http::{Request, StatusCode}, }; + use sqlx::PgPool; + use stack_up::Services; use tower::ServiceExt; - use crate::{server, state::AppState}; + use crate::{ + server::{self, test_config}, + state::AppState, + }; - #[tokio::test] - async fn health_check() { - let state = AppState::new().await.unwrap(); + #[sqlx::test] + async fn health_check(pool: PgPool) { + let services = Services { postgres: pool }; + let state = AppState::new(services, &test_config()).await.unwrap(); let app = server::router(state); let response = app diff --git a/src/server/routes/users.rs b/src/server/routes/users.rs index 2ef49b2..d3ce446 100644 --- a/src/server/routes/users.rs +++ b/src/server/routes/users.rs @@ -1,3 +1,4 @@ +pub mod get_outbox; pub mod get_user; pub mod webfinger; @@ -5,6 +6,7 @@ use axum::{Router, routing::get}; pub fn users_router() -> Router { Router::new() - .route("/users/{usernme}", get(get_user::http_get_user)) + .route("/users/{username}", get(get_user::http_get_user)) + .route("/users/{username}/outbox", get(get_outbox::http_get_outbox)) .route("/.well-known/webfinger", get(webfinger::webfinger)) } diff --git a/src/server/routes/users/get_outbox.rs b/src/server/routes/users/get_outbox.rs new file mode 100644 index 0000000..d5a4af5 --- /dev/null +++ b/src/server/routes/users/get_outbox.rs @@ -0,0 +1,77 @@ +use activitypub_federation::{ + axum::json::FederationJson, config::Data, protocol::context::WithContext, traits::Object, +}; +use axum::{debug_handler, extract::Path, http::StatusCode, response::IntoResponse}; + +use crate::{error::AppError, state::AppHandle}; + +#[debug_handler] +pub async fn http_get_outbox( + Path(name): Path<String>, + data: Data<AppHandle>, +) -> Result<impl IntoResponse, AppError> { + if let Some(a) = super::get_user::read_user(&name, &data).await? { + let json_user = a.into_json(&data).await?; + Ok(( + StatusCode::OK, + FederationJson(WithContext::new_default(json_user)), + ) + .into_response()) + } else { + Ok((StatusCode::NOT_FOUND, "").into_response()) + } +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use sqlx::PgPool; + use stack_up::Services; + use tower::ServiceExt; + + use crate::{ + server::{self, test_config}, + state::AppState, + }; + + #[sqlx::test] + async fn get_user(pool: PgPool) { + let services = Services { postgres: pool }; + let state = AppState::new(services, &test_config()).await.unwrap(); + let app = server::router(state); + + let response = app + .oneshot( + Request::builder() + .uri("/users/sellershut") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[sqlx::test] + async fn get_user_not_found(pool: PgPool) { + let services = Services { postgres: pool }; + let state = AppState::new(services, &test_config()).await.unwrap(); + let app = server::router(state); + + let response = app + .oneshot( + Request::builder() + .uri("/users/selut") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } +} diff --git a/src/server/routes/users/get_user.rs b/src/server/routes/users/get_user.rs index d86cc26..4079731 100644 --- a/src/server/routes/users/get_user.rs +++ b/src/server/routes/users/get_user.rs @@ -2,6 +2,7 @@ use activitypub_federation::{ axum::json::FederationJson, config::Data, protocol::context::WithContext, traits::Object, }; use axum::{debug_handler, extract::Path, http::StatusCode, response::IntoResponse}; +use tracing::trace; use crate::{error::AppError, state::AppHandle}; @@ -10,7 +11,7 @@ pub async fn http_get_user( Path(name): Path<String>, data: Data<AppHandle>, ) -> Result<impl IntoResponse, AppError> { - if let Some(a) = read_user(&name, &data).await { + if let Some(a) = read_user(&name, &data).await? { let json_user = a.into_json(&data).await?; Ok(( StatusCode::OK, @@ -22,11 +23,26 @@ pub async fn http_get_user( } } -pub async fn read_user(name: &str, data: &Data<AppHandle>) -> Option<crate::entity::user::User> { - let read = data.users.read().await; - read.iter() - .find(|value| value.username.eq(&name)) - .map(ToOwned::to_owned) +pub async fn read_user( + name: &str, + data: &Data<AppHandle>, +) -> Result<Option<crate::entity::user::User>, AppError> { + trace!(username = name, "getting user"); + let read = sqlx::query_as!( + crate::entity::user::DbUser, + "select * from account where username = $1 and local = $2", + name, + true + ) + .fetch_optional(&data.services.postgres) + .await?; + + let user = read.into_iter().find(|value| value.username.eq(&name)); + let user = match user { + Some(user) => Some(crate::entity::user::User::try_from(user)?), + None => None, + }; + Ok(user) } #[cfg(test)] @@ -35,13 +51,19 @@ mod tests { body::Body, http::{Request, StatusCode}, }; + use sqlx::PgPool; + use stack_up::Services; use tower::ServiceExt; - use crate::{server, state::AppState}; + use crate::{ + server::{self, test_config}, + state::AppState, + }; - #[tokio::test] - async fn get_user() { - let state = AppState::new().await.unwrap(); + #[sqlx::test] + async fn get_user(pool: PgPool) { + let services = Services { postgres: pool }; + let state = AppState::new(services, &test_config()).await.unwrap(); let app = server::router(state); let response = app @@ -57,9 +79,10 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } - #[tokio::test] - async fn get_user_not_found() { - let state = AppState::new().await.unwrap(); + #[sqlx::test] + async fn get_user_not_found(pool: PgPool) { + let services = Services { postgres: pool }; + let state = AppState::new(services, &test_config()).await.unwrap(); let app = server::router(state); let response = app diff --git a/src/server/routes/users/webfinger.rs b/src/server/routes/users/webfinger.rs index 22975c2..8efeda6 100644 --- a/src/server/routes/users/webfinger.rs +++ b/src/server/routes/users/webfinger.rs @@ -17,7 +17,7 @@ pub async fn webfinger( data: Data<AppHandle>, ) -> Result<impl IntoResponse, AppError> { let name = extract_webfinger_name(&query.resource, &data)?; - if let Some(db_user) = read_user(name, &data).await { + if let Some(db_user) = read_user(name, &data).await? { Ok(( StatusCode::OK, Json(build_webfinger_response( @@ -37,13 +37,19 @@ mod tests { body::Body, http::{Request, StatusCode}, }; + use sqlx::PgPool; + use stack_up::Services; use tower::ServiceExt; - use crate::{server, state::AppState}; + use crate::{ + server::{self, test_config}, + state::AppState, + }; - #[tokio::test] - async fn webfinger_ok() { - let state = AppState::new().await.unwrap(); + #[sqlx::test] + async fn webfinger_ok(pool: PgPool) { + let services = Services { postgres: pool }; + let state = AppState::new(services, &test_config()).await.unwrap(); let app = server::router(state); let response = app @@ -59,9 +65,10 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); } - #[tokio::test] - async fn webfinger_err() { - let state = AppState::new().await.unwrap(); + #[sqlx::test] + async fn webfinger_err(pool: PgPool) { + let services = Services { postgres: pool }; + let state = AppState::new(services, &test_config()).await.unwrap(); let app = server::router(state); let response = app diff --git a/src/state.rs b/src/state.rs index d7c9136..5ced62e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,7 @@ use std::{ops::Deref, sync::Arc}; use activitypub_federation::config::FederationConfig; -use tokio::sync::RwLock; +use stack_up::{Configuration, Environment, Services}; use crate::{entity::user::User, error::AppError}; @@ -17,23 +17,24 @@ impl Deref for AppHandle { } pub struct AppState { - pub users: RwLock<Vec<User>>, + pub services: Services, } impl AppState { - pub async fn new() -> Result<FederationConfig<AppHandle>, AppError> { - let user = User::new("sellershut")?; + pub async fn new( + services: Services, + configuration: &Configuration, + ) -> Result<FederationConfig<AppHandle>, AppError> { + let user = User::new("sellershut", &services).await?; let domain = "localhost"; let config = FederationConfig::builder() .domain(domain) .signed_fetch_actor(&user) - .app_data(AppHandle(Arc::new(Self { - users: RwLock::new(vec![user]), - }))) + .app_data(AppHandle(Arc::new(Self { services }))) // .url_verifier(Box::new(MyUrlVerifier())) // TODO: could change this to env variable? - .debug(cfg!(debug_assertions)) + .debug(configuration.application.env == Environment::Development) .build() .await?; |