use std::sync::Arc;
use axum::Router;
use axum::http::Method;
use axum::routing::{get, post};
use activitystreams_vocabulary::{Item, Items};
use crate::crypto::HttpPrivateKey;
use crate::db::{DbConfig, Iri, Name};
use crate::{Error, Result};
mod credentials;
mod factory;
mod middleware;
mod person;
mod repository;
mod state;
pub mod oauth;
pub use credentials::*;
pub use state::*;
use crate::middleware;
pub struct App {
state: Arc<AppState>,
}
impl App {
pub async fn create(config: DbConfig, uri: Iri, name: Name) -> Result<Self> {
AppState::create(config, uri, name)
.await
.map(|s| Self { state: Arc::new(s) })
}
pub fn state(&self) -> &AppState {
self.state.as_ref()
}
pub fn clone_state(&self) -> Arc<AppState> {
Arc::clone(&self.state)
}
#[inline]
pub fn uri(&self) -> &Iri {
self.state.uri()
}
#[inline]
pub fn oauth_callback_uri(uri: &Iri) -> Result<Iri> {
uri.base_iri()
.and_then(|base| Iri::try_from(format!("{base}/oauth/callback")))
}
pub async fn signed_request_with_keys<S: serde::ser::Serialize>(
keys: &[HttpPrivateKey],
method: Method,
uri: &Iri,
body: Option<&S>,
) -> Result<reqwest::Response> {
AppState::signed_request_with_keys(keys, method, uri, body).await
}
pub async fn router(&self) -> Result<Router> {
Ok(Router::new()
.route("/api/v1/factories/{uuid}/outbox", get(Self::factory_outbox_read))
.route("/api/v1/factories/{uuid}/outbox", post(Self::factory_outbox_write))
.route("/api/v1/factories/{uuid}", get(Self::get_factory))
.route("/api/v1/persons/{uuid}/inbox", get(Self::person_inbox_read))
.route("/api/v1/persons/{uuid}/inbox", post(Self::person_inbox_write))
.route("/api/v1/persons/{uuid}/outbox", get(Self::person_outbox_read))
.route("/api/v1/persons/{uuid}/outbox", post(Self::person_outbox_write))
.route("/api/v1/persons/{uuid}", get(Self::get_person))
.route("/api/v1/repositories/{uuid}/inbox", get(Self::repository_inbox_read))
.route("/api/v1/repositories/{uuid}/inbox", post(Self::repository_inbox_write))
.route("/api/v1/repositories/{uuid}/outbox", get(Self::repository_outbox_read))
.route("/api/v1/repositories/{uuid}/outbox", post(Self::repository_outbox_write))
.route("/api/v1/repositories/{uuid}", get(Self::get_repository))
.route_layer(
middleware!(Self: self.clone_state() => { oauth_or_httpsig_handler, fetch_actor_handler }),
)
.route("/oauth/authenticate", post(Self::oauth_authenticate))
.route("/oauth/register", post(Self::oauth_register))
.route("/oauth/authorize", get(Self::oauth_authorize))
.route("/oauth/callback", get(Self::oauth_callback))
.route("/oauth/token", post(Self::oauth_token))
.route("/oauth/refresh", post(Self::oauth_refresh))
.with_state(self.clone_state()))
}
#[cfg(feature = "e2e-tests")]
pub async fn test_router(&self) -> Result<Router> {
use axum::extract::State;
Ok(Router::new()
.route("/fetch-actor", get(|_: State<Arc<AppState>>| async { "Test HTTP Message Signature + fetch actor handler" }))
.route_layer(
middleware!(Self: self.clone_state() => { http_signature_handler, fetch_actor_handler }),
)
.route("/signature", get(|_: State<Arc<AppState>>| async { "Test HTTP Message Signature handler" }))
.route_layer(
middleware!(Self: self.clone_state() => { http_signature_handler }),
)
.route("/", get(|_: State<Arc<AppState>>| async { "Test ROOT handler" }))
.with_state(self.clone_state()))
}
fn check_activity_actor_id(context: &str, actor_id: &Iri, vocab_actor: &Items) -> Result<()> {
match vocab_actor {
Items::Single(Item::Iri(id)) => {
let activity_actor_id = id.as_ref().into();
if actor_id == &activity_actor_id {
Ok(())
} else {
Err(Error::http(format!(
"{context}: mismatch of Activity actor ID: {activity_actor_id} and signer ID: {actor_id}"
)))
}
}
Items::Single(Item::Object(obj)) => {
let activity_actor_id = obj.id().map(|i| i.into()).unwrap_or_default();
if actor_id == &activity_actor_id {
Ok(())
} else {
Err(Error::http(format!(
"{context}: mismatch of Activity actor ID: {activity_actor_id} and signer ID: {actor_id}"
)))
}
}
Items::Single(Item::Link(link)) => {
let activity_actor_id = link.href().into();
if actor_id == &activity_actor_id {
Ok(())
} else {
Err(Error::http(format!(
"{context}: mismatch of Activity actor ID: {activity_actor_id} and signer ID: {actor_id}"
)))
}
}
Items::List(list) => {
if list.iter().any(|i| match i {
Item::Iri(id) => actor_id.as_str() == id.as_str(),
Item::Object(obj) => obj
.id()
.map(|i| actor_id.as_str() == i.as_str())
.unwrap_or_default(),
Item::Link(link) => link.href().as_str() == actor_id.as_str(),
}) {
Ok(())
} else {
Err(Error::http(format!(
"{context}: no matching Activity actor ID for ID: {actor_id}",
)))
}
}
}
}
}