about summary refs log tree commit diff
path: root/kittybox-rs/src/micropub/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'kittybox-rs/src/micropub/mod.rs')
-rw-r--r--kittybox-rs/src/micropub/mod.rs148
1 files changed, 82 insertions, 66 deletions
diff --git a/kittybox-rs/src/micropub/mod.rs b/kittybox-rs/src/micropub/mod.rs
index d0aeae0..0f7441e 100644
--- a/kittybox-rs/src/micropub/mod.rs
+++ b/kittybox-rs/src/micropub/mod.rs
@@ -1,16 +1,16 @@
 use crate::database::{MicropubChannel, Storage, StorageError};
-use crate::tokenauth::User;
+use crate::indieauth::backend::AuthBackend;
+use crate::indieauth::User;
 use crate::micropub::util::form_to_mf2_json;
-use axum::extract::{BodyStream, Query};
+use axum::extract::{BodyStream, Query, Host};
 use axum::headers::ContentType;
 use axum::response::{IntoResponse, Response};
 use axum::TypedHeader;
 use axum::{http::StatusCode, Extension};
 use serde::{Deserialize, Serialize};
 use serde_json::json;
-use std::fmt::Display;
 use tracing::{debug, error, info, warn};
-
+use kittybox_indieauth::{Scope, TokenData};
 use kittybox_util::{MicropubError, ErrorType};
 
 #[derive(Serialize, Deserialize, Debug, PartialEq)]
@@ -65,8 +65,7 @@ fn populate_reply_context(
                 .iter()
                 .find(|ctx| Some(ctx.url.as_str()) == i.as_str())
                 .and_then(|ctx| ctx.mf2["items"].get(0))
-                .or(Some(i))
-                .unwrap())
+                .unwrap_or(i))
             .collect::<Vec<&serde_json::Value>>())
     })
 }
@@ -218,7 +217,7 @@ async fn background_processing<D: 'static + Storage>(
 
 // TODO actually save the post to the database and schedule post-processing
 pub(crate) async fn _post<D: 'static + Storage>(
-    user: User,
+    user: &TokenData,
     uid: String,
     mf2: serde_json::Value,
     db: D,
@@ -234,7 +233,7 @@ pub(crate) async fn _post<D: 'static + Storage>(
     //   - The MF2-JSON document's author is set
 
     // Security check! Do we have an OAuth2 scope to proceed?
-    if !user.check_scope("create") {
+    if !user.check_scope(&Scope::Create) {
         return Err(MicropubError {
             error: ErrorType::InvalidScope,
             error_description: "Not enough privileges - try acquiring the \"create\" scope."
@@ -292,7 +291,7 @@ pub(crate) async fn _post<D: 'static + Storage>(
             db.update_post(chan, json!({"add": {"children": [uid]}}))
                 .await?;
         } else if default_channels.iter().any(|i| chan == i) {
-            util::create_feed(&db, &uid, chan, &user).await?;
+            util::create_feed(&db, &uid, chan, user).await?;
         } else {
             warn!("Ignoring non-existent channel: {}", chan);
         }
@@ -344,10 +343,10 @@ impl From<MicropubFormAction> for MicropubAction {
 }
 
 #[tracing::instrument(skip(db))]
-async fn post_action<D: Storage>(
+async fn post_action<D: Storage, A: AuthBackend>(
     action: MicropubAction,
     db: D,
-    user: User,
+    user: User<A>,
 ) -> Result<(), MicropubError> {
     let uri = if let Ok(uri) = action.url.parse::<hyper::Uri>() {
         uri
@@ -375,7 +374,7 @@ async fn post_action<D: Storage>(
 
     match action.action {
         ActionType::Delete => {
-            if !user.check_scope("delete") {
+            if !user.check_scope(&Scope::Delete) {
                 return Err(MicropubError {
                     error: ErrorType::InvalidScope,
                     error_description: "You need a \"delete\" scope for this.".to_owned(),
@@ -385,7 +384,7 @@ async fn post_action<D: Storage>(
             db.delete_post(&action.url).await?
         }
         ActionType::Update => {
-            if !user.check_scope("update") {
+            if !user.check_scope(&Scope::Update) {
                 return Err(MicropubError {
                     error: ErrorType::InvalidScope,
                     error_description: "You need an \"update\" scope for this.".to_owned(),
@@ -468,10 +467,10 @@ async fn dispatch_body(
 }
 
 #[tracing::instrument(skip(db, http))]
-pub async fn post<D: Storage + 'static>(
+pub(crate) async fn post<D: Storage + 'static, A: AuthBackend>(
     Extension(db): Extension<D>,
     Extension(http): Extension<reqwest::Client>,
-    user: User,
+    user: User<A>,
     body: BodyStream,
     TypedHeader(content_type): TypedHeader<ContentType>,
 ) -> axum::response::Response {
@@ -482,7 +481,7 @@ pub async fn post<D: Storage + 'static>(
         },
         Ok(PostBody::MF2(mf2)) => {
             let (uid, mf2) = normalize_mf2(mf2, &user);
-            match _post(user, uid, mf2, db, http).await {
+            match _post(&user, uid, mf2, db, http).await {
                 Ok(response) => response,
                 Err(err) => err.into_response(),
             }
@@ -492,30 +491,40 @@ pub async fn post<D: Storage + 'static>(
 }
 
 #[tracing::instrument(skip(db))]
-pub async fn query<D: Storage>(
+pub(crate) async fn query<D: Storage, A: AuthBackend>(
     Extension(db): Extension<D>,
     query: Option<Query<MicropubQuery>>,
-    user: User,
+    Host(host): Host,
+    user: User<A>,
 ) -> axum::response::Response {
     // We handle the invalid query case manually to return a
     // MicropubError instead of HTTP 422
-    if query.is_none() {
+    let query = if let Some(Query(query)) = query {
+        query
+    } else {
         return MicropubError::new(
             ErrorType::InvalidRequest,
             "Invalid query provided. Try ?q=config to see what you can do."
         ).into_response();
-    }
-    let query: MicropubQuery = query.unwrap().0;
+    };
 
-    let host = axum::http::Uri::try_from(user.me.as_str())
+    if axum::http::Uri::try_from(user.me.as_str())
         .unwrap()
         .authority()
         .unwrap()
-        .clone();
+        != &host
+    {
+        return MicropubError::new(
+            ErrorType::NotAuthorized,
+            "This website doesn't belong to you.",
+        )
+            .into_response();
+    }
 
+    
     match query.q {
         QueryType::Config => {
-            let channels: Vec<MicropubChannel> = match db.get_channels(host.as_str()).await {
+            let channels: Vec<MicropubChannel> = match db.get_channels(user.me.as_str()).await {
                 Ok(chans) => chans,
                 Err(err) => {
                     return MicropubError::new(
@@ -534,26 +543,15 @@ pub async fn query<D: Storage>(
                     QueryType::SyndicateTo
                 ],
                 "channels": channels,
-                "_kittybox_authority": host.as_str(),
-                "syndicate-to": []
+                "_kittybox_authority": user.me.as_str(),
+                "syndicate-to": [],
+                "media_endpoint": user.me.join("/.kittybox/media").unwrap().as_str()
             }))
             .into_response()
         }
         QueryType::Source => {
             match query.url {
                 Some(url) => {
-                    if axum::http::Uri::try_from(&url)
-                        .unwrap()
-                        .authority()
-                        .unwrap()
-                        != &host
-                    {
-                        return MicropubError::new(
-                            ErrorType::NotAuthorized,
-                            "You are requesting a post from a website that doesn't belong to you.",
-                        )
-                        .into_response();
-                    }
                     match db.get_post(&url).await {
                         Ok(some) => match some {
                             Some(post) => axum::response::Json(&post).into_response(),
@@ -582,7 +580,7 @@ pub async fn query<D: Storage>(
                 }
             }
         }
-        QueryType::Channel => match db.get_channels(host.as_str()).await {
+        QueryType::Channel => match db.get_channels(user.me.as_str()).await {
             Ok(chans) => axum::response::Json(json!({ "channels": chans })).into_response(),
             Err(err) => MicropubError::new(
                 ErrorType::InternalServerError,
@@ -596,9 +594,17 @@ pub async fn query<D: Storage>(
     }
 }
 
-pub fn router<S: Storage + 'static>(storage: S, http: reqwest::Client) -> axum::routing::MethodRouter {
-    axum::routing::get(query::<S>)
-        .post(post::<S>)
+pub fn router<S, A>(
+    storage: S,
+    http: reqwest::Client,
+    auth: A
+) -> axum::routing::MethodRouter
+where
+    S: Storage + 'static,
+    A: AuthBackend
+{
+    axum::routing::get(query::<S, A>)
+        .post(post::<S, A>)
         .layer(tower_http::cors::CorsLayer::new()
                .allow_methods([
                    axum::http::Method::GET,
@@ -607,6 +613,7 @@ pub fn router<S: Storage + 'static>(storage: S, http: reqwest::Client) -> axum::
                .allow_origin(tower_http::cors::Any))
         .layer(axum::Extension(storage))
         .layer(axum::Extension(http))
+        .layer(axum::Extension(auth))
 }
 
 #[cfg(test)]
@@ -629,11 +636,13 @@ impl MicropubQuery {
 
 #[cfg(test)]
 mod tests {
-    use crate::{database::Storage, micropub::MicropubError, tokenauth::User};
+    use crate::{database::Storage, micropub::MicropubError};
     use hyper::body::HttpBody;
     use serde_json::json;
 
     use super::FetchedPostContext;
+    use kittybox_indieauth::{Scopes, Scope, TokenData};
+    use axum::extract::Host;
 
     #[test]
     fn test_populate_reply_context() {
@@ -682,14 +691,15 @@ mod tests {
                 "content": ["Hello world!"]
             }
         });
-        let user = User::new(
-            "https://localhost:8080/",
-            "https://kittybox.fireburn.ru/",
-            "profile",
-        );
+        let user = TokenData {
+            me: "https://localhost:8080/".parse().unwrap(),
+            client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
+            scope: Scopes::new(vec![Scope::Profile]),
+            iat: None, exp: None
+        };
         let (uid, mf2) = super::normalize_mf2(post, &user);
 
-        let err = super::_post(user, uid, mf2, db.clone(), reqwest::Client::new())
+        let err = super::_post(&user, uid, mf2, db.clone(), reqwest::Client::new())
             .await
             .unwrap_err();
 
@@ -711,14 +721,15 @@ mod tests {
                 "url": ["https://fireburn.ru/posts/hello"]
             }
         });
-        let user = User::new(
-            "https://aaronparecki.com/",
-            "https://kittybox.fireburn.ru/",
-            "create update media",
-        );
+        let user = TokenData {
+            me: "https://aaronparecki.com/".parse().unwrap(),
+            client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
+            scope: Scopes::new(vec![Scope::Profile, Scope::Create, Scope::Update, Scope::Media]),
+            iat: None, exp: None
+        };
         let (uid, mf2) = super::normalize_mf2(post, &user);
 
-        let err = super::_post(user, uid, mf2, db.clone(), reqwest::Client::new())
+        let err = super::_post(&user, uid, mf2, db.clone(), reqwest::Client::new())
             .await
             .unwrap_err();
 
@@ -738,14 +749,15 @@ mod tests {
                 "content": ["Hello world!"]
             }
         });
-        let user = User::new(
-            "https://localhost:8080/",
-            "https://kittybox.fireburn.ru/",
-            "create",
-        );
+        let user = TokenData {
+            me: "https://localhost:8080/".parse().unwrap(),
+            client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
+            scope: Scopes::new(vec![Scope::Profile, Scope::Create]),
+            iat: None, exp: None
+        };
         let (uid, mf2) = super::normalize_mf2(post, &user);
 
-        let res = super::_post(user, uid, mf2, db.clone(), reqwest::Client::new())
+        let res = super::_post(&user, uid, mf2, db.clone(), reqwest::Client::new())
             .await
             .unwrap();
 
@@ -765,11 +777,15 @@ mod tests {
             Some(axum::extract::Query(super::MicropubQuery::source(
                 "https://aaronparecki.com/feeds/main",
             ))),
-            User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            ),
+            Host("aaronparecki.com".to_owned()),
+            crate::indieauth::User::<crate::indieauth::backend::fs::FileBackend>(
+                TokenData {
+                    me: "https://fireburn.ru/".parse().unwrap(),
+                    client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
+                    scope: Scopes::new(vec![Scope::Profile, Scope::Create, Scope::Update, Scope::Media]),
+                    iat: None, exp: None
+                }, std::marker::PhantomData
+            )
         )
         .await;