about summary refs log tree commit diff
path: root/src/login.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2025-04-09 23:31:02 +0300
committerVika <vika@fireburn.ru>2025-04-09 23:31:57 +0300
commit8826d9446e6c492db2243b9921e59ce496027bef (patch)
tree63738aa9001cb73b11cb0e974e93129bcdf1adbb /src/login.rs
parent519cadfbb298f50cbf819dde757037ab56e2863e (diff)
downloadkittybox-8826d9446e6c492db2243b9921e59ce496027bef.tar.zst
cargo fmt
Change-Id: I80e81ebba3f0cdf8c094451c9fe3ee4126b8c888
Diffstat (limited to 'src/login.rs')
-rw-r--r--src/login.rs266
1 files changed, 189 insertions, 77 deletions
diff --git a/src/login.rs b/src/login.rs
index eaa787c..3038d9c 100644
--- a/src/login.rs
+++ b/src/login.rs
@@ -1,10 +1,25 @@
 use std::{borrow::Cow, str::FromStr};
 
+use axum::{
+    extract::{FromRef, Query, State},
+    http::HeaderValue,
+    response::IntoResponse,
+    Form,
+};
+use axum_extra::{
+    extract::{
+        cookie::{self, Cookie},
+        Host, SignedCookieJar,
+    },
+    headers::HeaderMapExt,
+    TypedHeader,
+};
 use futures_util::FutureExt;
-use axum::{extract::{FromRef, Query, State}, http::HeaderValue, response::IntoResponse, Form};
-use axum_extra::{extract::{Host, cookie::{self, Cookie}, SignedCookieJar}, headers::HeaderMapExt, TypedHeader};
-use hyper::{header::{CACHE_CONTROL, LOCATION}, StatusCode};
-use kittybox_frontend_renderer::{Template, LoginPage, LogoutPage};
+use hyper::{
+    header::{CACHE_CONTROL, LOCATION},
+    StatusCode,
+};
+use kittybox_frontend_renderer::{LoginPage, LogoutPage, Template};
 use kittybox_indieauth::{AuthorizationResponse, Error, GrantType, PKCEVerifier, Scope, Scopes};
 use sha2::Digest;
 
@@ -13,14 +28,13 @@ use crate::database::Storage;
 /// Show a login page.
 async fn get<S: Storage + Send + Sync + 'static>(
     State(db): State<S>,
-    Host(host): Host
+    Host(host): Host,
 ) -> impl axum::response::IntoResponse {
     let hcard_url: url::Url = format!("https://{}/", host).parse().unwrap();
 
     let (blogname, channels) = tokio::join!(
         db.get_setting::<crate::database::settings::SiteName>(&hcard_url)
-        .map(Result::unwrap_or_default),
-
+            .map(Result::unwrap_or_default),
         db.get_channels(&hcard_url).map(|i| i.unwrap_or_default())
     );
     (
@@ -34,14 +48,15 @@ async fn get<S: Storage + Send + Sync + 'static>(
             blog_name: blogname.as_ref(),
             feeds: channels,
             user: None,
-            content: LoginPage {}.to_string()
-        }.to_string()
+            content: LoginPage {}.to_string(),
+        }
+        .to_string(),
     )
 }
 
 #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
 struct LoginForm {
-    url: url::Url
+    url: url::Url,
 }
 
 /// Accept login and start the IndieAuth dance.
@@ -60,10 +75,12 @@ async fn post(
             .expires(None)
             .secure(true)
             .http_only(true)
-            .build()
+            .build(),
     );
 
-    let client_id: url::Url = format!("https://{}/.kittybox/login/client_metadata", host).parse().unwrap();
+    let client_id: url::Url = format!("https://{}/.kittybox/login/client_metadata", host)
+        .parse()
+        .unwrap();
     let redirect_uri = {
         let mut uri = client_id.clone();
         uri.set_path("/.kittybox/login/finish");
@@ -71,11 +88,15 @@ async fn post(
     };
     let indieauth_state = kittybox_indieauth::AuthorizationRequest {
         response_type: kittybox_indieauth::ResponseType::Code,
-        client_id, redirect_uri,
+        client_id,
+        redirect_uri,
         state: kittybox_indieauth::State::new(),
-        code_challenge: kittybox_indieauth::PKCEChallenge::new(&code_verifier, kittybox_indieauth::PKCEMethod::S256),
+        code_challenge: kittybox_indieauth::PKCEChallenge::new(
+            &code_verifier,
+            kittybox_indieauth::PKCEMethod::S256,
+        ),
         scope: Some(Scopes::new(vec![Scope::Profile])),
-        me: Some(form.url.clone())
+        me: Some(form.url.clone()),
     };
 
     // Fetch the user's homepage, determine their authorization endpoint
@@ -89,8 +110,9 @@ async fn post(
             tracing::error!("Error fetching homepage: {:?}", err);
             return (
                 StatusCode::BAD_REQUEST,
-                format!("couldn't fetch your homepage: {}", err)
-            ).into_response()
+                format!("couldn't fetch your homepage: {}", err),
+            )
+                .into_response();
         }
     };
 
@@ -106,22 +128,27 @@ async fn post(
     //     .collect::<Vec<axum_extra::headers::Link>>();
     //
     // todo!("parse Link: headers")
-    
+
     let body = match response.text().await {
         Ok(body) => match microformats::from_html(&body, form.url) {
             Ok(mf2) => mf2,
-            Err(err) => return (
-                StatusCode::BAD_REQUEST,
-                format!("error while parsing your homepage with mf2: {}", err)
-            ).into_response()
+            Err(err) => {
+                return (
+                    StatusCode::BAD_REQUEST,
+                    format!("error while parsing your homepage with mf2: {}", err),
+                )
+                    .into_response()
+            }
         },
-        Err(err) => return (
-            StatusCode::INTERNAL_SERVER_ERROR,
-            format!("error while fetching your homepage: {}", err)
-        ).into_response()
+        Err(err) => {
+            return (
+                StatusCode::INTERNAL_SERVER_ERROR,
+                format!("error while fetching your homepage: {}", err),
+            )
+                .into_response()
+        }
     };
 
-
     let mut iss: Option<url::Url> = None;
     let mut authorization_endpoint = match body
         .rels
@@ -139,10 +166,22 @@ async fn post(
                 Ok(metadata) => {
                     iss = Some(metadata.issuer);
                     metadata.authorization_endpoint
-                },
-                Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't parse your oauth2 metadata: {}", err)).into_response()
+                }
+                Err(err) => {
+                    return (
+                        StatusCode::INTERNAL_SERVER_ERROR,
+                        format!("couldn't parse your oauth2 metadata: {}", err),
+                    )
+                        .into_response()
+                }
             },
-            Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't fetch your oauth2 metadata: {}", err)).into_response()
+            Err(err) => {
+                return (
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                    format!("couldn't fetch your oauth2 metadata: {}", err),
+                )
+                    .into_response()
+            }
         },
         None => match body
             .rels
@@ -151,13 +190,17 @@ async fn post(
             .map(|v| v.as_slice())
             .unwrap_or_default()
             .first()
-            .cloned() {
-                Some(authorization_endpoint) => authorization_endpoint,
-                None => return (
+            .cloned()
+        {
+            Some(authorization_endpoint) => authorization_endpoint,
+            None => {
+                return (
                     StatusCode::BAD_REQUEST,
-                    "no authorization endpoint was found on your homepage."
-                ).into_response()
+                    "no authorization endpoint was found on your homepage.",
+                )
+                    .into_response()
             }
+        },
     };
 
     cookies = cookies.add(
@@ -166,7 +209,7 @@ async fn post(
             .expires(None)
             .secure(true)
             .http_only(true)
-            .build()
+            .build(),
     );
 
     if let Some(iss) = iss {
@@ -176,7 +219,7 @@ async fn post(
                 .expires(None)
                 .secure(true)
                 .http_only(true)
-                .build()
+                .build(),
         );
     }
 
@@ -186,7 +229,7 @@ async fn post(
             .expires(None)
             .secure(true)
             .http_only(true)
-            .build()
+            .build(),
     );
 
     authorization_endpoint
@@ -194,9 +237,12 @@ async fn post(
         .extend_pairs(indieauth_state.as_query_pairs().iter());
 
     tracing::debug!("Forwarding user to {}", authorization_endpoint);
-    (StatusCode::FOUND, [
-        ("Location", authorization_endpoint.to_string()),
-    ], cookies).into_response()
+    (
+        StatusCode::FOUND,
+        [("Location", authorization_endpoint.to_string())],
+        cookies,
+    )
+        .into_response()
 }
 
 /// Accept the return of the IndieAuth dance. Set a cookie for the
@@ -208,7 +254,9 @@ async fn callback(
     State(http): State<reqwest_middleware::ClientWithMiddleware>,
     State(session_store): State<crate::SessionStore>,
 ) -> axum::response::Response {
-    let client_id: url::Url = format!("https://{}/.kittybox/login/client_metadata", host).parse().unwrap();
+    let client_id: url::Url = format!("https://{}/.kittybox/login/client_metadata", host)
+        .parse()
+        .unwrap();
     let redirect_uri = {
         let mut uri = client_id.clone();
         uri.set_path("/.kittybox/login/finish");
@@ -218,7 +266,8 @@ async fn callback(
 
     let me: url::Url = cookie_jar.get("me").unwrap().value().parse().unwrap();
     let code_verifier: PKCEVerifier = cookie_jar.get("code_verifier").unwrap().value().into();
-    let authorization_endpoint: url::Url = cookie_jar.get("authorization_endpoint")
+    let authorization_endpoint: url::Url = cookie_jar
+        .get("authorization_endpoint")
         .and_then(|v| v.value().parse().ok())
         .unwrap();
     match cookie_jar.get("iss").and_then(|c| c.value().parse().ok()) {
@@ -232,24 +281,59 @@ async fn callback(
         code: response.code,
         client_id,
         redirect_uri,
-        code_verifier, 
+        code_verifier,
     };
-    tracing::debug!("POSTing {:?} to authorization endpoint {}", grant_request, authorization_endpoint);
-    let res = match http.post(authorization_endpoint)
+    tracing::debug!(
+        "POSTing {:?} to authorization endpoint {}",
+        grant_request,
+        authorization_endpoint
+    );
+    let res = match http
+        .post(authorization_endpoint)
         .form(&grant_request)
         .header(reqwest::header::ACCEPT, "application/json")
         .send()
         .await
     {
-        Ok(res) if res.status().is_success() => match res.json::<kittybox_indieauth::GrantResponse>().await {
-            Ok(grant) => grant,
-            Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, [(CACHE_CONTROL, "no-store")], format!("error parsing authorization endpoint response: {}", err)).into_response()
-        },
+        Ok(res) if res.status().is_success() => {
+            match res.json::<kittybox_indieauth::GrantResponse>().await {
+                Ok(grant) => grant,
+                Err(err) => {
+                    return (
+                        StatusCode::INTERNAL_SERVER_ERROR,
+                        [(CACHE_CONTROL, "no-store")],
+                        format!("error parsing authorization endpoint response: {}", err),
+                    )
+                        .into_response()
+                }
+            }
+        }
         Ok(res) => match res.json::<Error>().await {
-            Ok(err) => return (StatusCode::BAD_REQUEST, [(CACHE_CONTROL, "no-store")], err.to_string()).into_response(),
-            Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, [(CACHE_CONTROL, "no-store")], format!("error parsing indieauth error: {}", err)).into_response()
+            Ok(err) => {
+                return (
+                    StatusCode::BAD_REQUEST,
+                    [(CACHE_CONTROL, "no-store")],
+                    err.to_string(),
+                )
+                    .into_response()
+            }
+            Err(err) => {
+                return (
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                    [(CACHE_CONTROL, "no-store")],
+                    format!("error parsing indieauth error: {}", err),
+                )
+                    .into_response()
+            }
+        },
+        Err(err) => {
+            return (
+                StatusCode::INTERNAL_SERVER_ERROR,
+                [(CACHE_CONTROL, "no-store")],
+                format!("error redeeming authorization code: {}", err),
+            )
+                .into_response()
         }
-        Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, [(CACHE_CONTROL, "no-store")], format!("error redeeming authorization code: {}", err)).into_response()
     };
 
     let profile = match res {
@@ -265,19 +349,28 @@ async fn callback(
     let uuid = uuid::Uuid::new_v4();
     session_store.write().await.insert(uuid, session);
     let cookies = cookie_jar
-        .add(Cookie::build(("session_id", uuid.to_string()))
-            .expires(None)
-            .secure(true)
-            .http_only(true)
-            .path("/")
-            .build()
+        .add(
+            Cookie::build(("session_id", uuid.to_string()))
+                .expires(None)
+                .secure(true)
+                .http_only(true)
+                .path("/")
+                .build(),
         )
         .remove("authorization_endpoint")
         .remove("me")
         .remove("iss")
         .remove("code_verifier");
 
-    (StatusCode::FOUND, [(LOCATION, HeaderValue::from_static("/")), (CACHE_CONTROL, HeaderValue::from_static("no-store"))], dbg!(cookies)).into_response()
+    (
+        StatusCode::FOUND,
+        [
+            (LOCATION, HeaderValue::from_static("/")),
+            (CACHE_CONTROL, HeaderValue::from_static("no-store")),
+        ],
+        dbg!(cookies),
+    )
+        .into_response()
 }
 
 /// Show the form necessary for logout. If JS is enabled,
@@ -288,32 +381,42 @@ async fn callback(
 /// stupid enough to execute JS and send a POST request though, that's
 /// on the crawler.
 async fn logout_page() -> impl axum::response::IntoResponse {
-    (StatusCode::OK, [("Content-Type", "text/html")], Template {
-        title: "Signing out...",
-        blog_name: "Kittybox",
-        feeds: vec![],
-        user: None,
-        content: LogoutPage {}.to_string()
-    }.to_string())
+    (
+        StatusCode::OK,
+        [("Content-Type", "text/html")],
+        Template {
+            title: "Signing out...",
+            blog_name: "Kittybox",
+            feeds: vec![],
+            user: None,
+            content: LogoutPage {}.to_string(),
+        }
+        .to_string(),
+    )
 }
 
 /// Erase the necessary cookies for login and invalidate the session.
 async fn logout(
     mut cookies: SignedCookieJar,
-    State(session_store): State<crate::SessionStore>
-) -> (StatusCode, [(&'static str, &'static str); 1], SignedCookieJar) {
-    if let Some(id) = cookies.get("session_id")
+    State(session_store): State<crate::SessionStore>,
+) -> (
+    StatusCode,
+    [(&'static str, &'static str); 1],
+    SignedCookieJar,
+) {
+    if let Some(id) = cookies
+        .get("session_id")
         .and_then(|c| uuid::Uuid::parse_str(c.value_trimmed()).ok())
     {
         session_store.write().await.remove(&id);
     }
-    cookies = cookies.remove("me")
+    cookies = cookies
+        .remove("me")
         .remove("iss")
         .remove("authorization_endpoint")
         .remove("code_verifier")
         .remove("session_id");
 
-    
     (StatusCode::FOUND, [("Location", "/")], cookies)
 }
 
@@ -343,7 +446,7 @@ async fn client_metadata<S: Storage + Send + Sync + 'static>(
     };
     if let Some(cached) = cached {
         if cached.precondition_passes(&etag) {
-            return StatusCode::NOT_MODIFIED.into_response()
+            return StatusCode::NOT_MODIFIED.into_response();
         }
     }
     let client_uri: url::Url = format!("https://{}/", host).parse().unwrap();
@@ -356,7 +459,13 @@ async fn client_metadata<S: Storage + Send + Sync + 'static>(
 
     let mut metadata = kittybox_indieauth::ClientMetadata::new(client_id, client_uri).unwrap();
 
-    metadata.client_name = Some(storage.get_setting::<crate::database::settings::SiteName>(&metadata.client_uri).await.unwrap_or_default().0);
+    metadata.client_name = Some(
+        storage
+            .get_setting::<crate::database::settings::SiteName>(&metadata.client_uri)
+            .await
+            .unwrap_or_default()
+            .0,
+    );
     metadata.grant_types = Some(vec![GrantType::AuthorizationCode]);
     // We don't request anything more than the profile scope.
     metadata.scope = Some(Scopes::new(vec![Scope::Profile]));
@@ -368,15 +477,18 @@ async fn client_metadata<S: Storage + Send + Sync + 'static>(
     // identity providers, or json to match newest spec
     let mut response = metadata.into_response();
     // Indicate to upstream caches this endpoint does different things depending on the Accept: header.
-    response.headers_mut().append("Vary", HeaderValue::from_static("Accept"));
+    response
+        .headers_mut()
+        .append("Vary", HeaderValue::from_static("Accept"));
     // Cache this metadata for an hour.
-    response.headers_mut().append("Cache-Control", HeaderValue::from_static("max-age=600"));
+    response
+        .headers_mut()
+        .append("Cache-Control", HeaderValue::from_static("max-age=600"));
     response.headers_mut().typed_insert(etag);
 
     response
 }
 
-
 /// Produce a router for all of the above.
 pub fn router<St, S>() -> axum::routing::Router<St>
 where