about summary refs log tree commit diff
path: root/src/frontend/onboarding.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/onboarding.rs')
-rw-r--r--src/frontend/onboarding.rs181
1 files changed, 181 insertions, 0 deletions
diff --git a/src/frontend/onboarding.rs b/src/frontend/onboarding.rs
new file mode 100644
index 0000000..e44e866
--- /dev/null
+++ b/src/frontend/onboarding.rs
@@ -0,0 +1,181 @@
+use std::sync::Arc;
+
+use crate::database::{settings, Storage};
+use axum::{
+    extract::{Extension, Host},
+    http::StatusCode,
+    response::{Html, IntoResponse},
+    Json,
+};
+use kittybox_frontend_renderer::{ErrorPage, OnboardingPage, Template};
+use serde::Deserialize;
+use tokio::{task::JoinSet, sync::Mutex};
+use tracing::{debug, error};
+
+use super::FrontendError;
+
+pub async fn get() -> Html<String> {
+    Html(
+        Template {
+            title: "Kittybox - Onboarding",
+            blog_name: "Kittybox",
+            feeds: vec![],
+            user: None,
+            content: OnboardingPage {}.to_string(),
+        }
+        .to_string(),
+    )
+}
+
+#[derive(Deserialize, Debug)]
+struct OnboardingFeed {
+    slug: String,
+    name: String,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OnboardingData {
+    user: serde_json::Value,
+    first_post: serde_json::Value,
+    #[serde(default = "OnboardingData::default_blog_name")]
+    blog_name: String,
+    feeds: Vec<OnboardingFeed>,
+}
+
+impl OnboardingData {
+    fn default_blog_name() -> String {
+        "Kitty Box!".to_owned()
+    }
+}
+
+#[tracing::instrument(skip(db, http))]
+async fn onboard<D: Storage + 'static>(
+    db: D,
+    user_uid: url::Url,
+    data: OnboardingData,
+    http: reqwest::Client,
+    jobset: Arc<Mutex<JoinSet<()>>>,
+) -> Result<(), FrontendError> {
+    // Create a user to pass to the backend
+    // At this point the site belongs to nobody, so it is safe to do
+    tracing::debug!("Creating user...");
+    let user = kittybox_indieauth::TokenData {
+        me: user_uid.clone(),
+        client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
+        scope: kittybox_indieauth::Scopes::new(vec![kittybox_indieauth::Scope::Create]),
+        iat: None, exp: None
+    };
+    tracing::debug!("User data: {:?}", user);
+
+    if data.user["type"][0] != "h-card" || data.first_post["type"][0] != "h-entry" {
+        return Err(FrontendError::with_code(
+            StatusCode::BAD_REQUEST,
+            "user and first_post should be an h-card and an h-entry",
+        ));
+    }
+
+    tracing::debug!("Setting settings...");
+    let user_domain = format!(
+        "{}{}",
+        user.me.host_str().unwrap(),
+        user.me.port()
+            .map(|port| format!(":{}", port))
+            .unwrap_or_default()
+    );
+    db.set_setting::<settings::SiteName>(&user_domain, data.blog_name.to_owned())
+        .await
+        .map_err(FrontendError::from)?;
+
+    db.set_setting::<settings::Webring>(&user_domain, false)
+        .await
+        .map_err(FrontendError::from)?;
+
+    let (_, hcard) = {
+        let mut hcard = data.user;
+        hcard["properties"]["uid"] = serde_json::json!([&user_uid]);
+        crate::micropub::normalize_mf2(hcard, &user)
+    };
+    db.put_post(&hcard, user_domain.as_str())
+        .await
+        .map_err(FrontendError::from)?;
+
+    debug!("Creating feeds...");
+    for feed in data.feeds {
+        if feed.name.is_empty() || feed.slug.is_empty() {
+            continue;
+        };
+        debug!("Creating feed {} with slug {}", &feed.name, &feed.slug);
+        let (_, feed) = crate::micropub::normalize_mf2(
+            serde_json::json!({
+                "type": ["h-feed"],
+                "properties": {"name": [feed.name], "mp-slug": [feed.slug]}
+            }),
+            &user,
+        );
+
+        db.put_post(&feed, user_uid.as_str())
+            .await
+            .map_err(FrontendError::from)?;
+    }
+    let (uid, post) = crate::micropub::normalize_mf2(data.first_post, &user);
+    tracing::debug!("Posting first post {}...", uid);
+    crate::micropub::_post(&user, uid, post, db, http, jobset)
+        .await
+        .map_err(|e| FrontendError {
+            msg: "Error while posting the first post".to_string(),
+            source: Some(Box::new(e)),
+            code: StatusCode::INTERNAL_SERVER_ERROR,
+        })?;
+
+    Ok(())
+}
+
+pub async fn post<D: Storage + 'static>(
+    Extension(db): Extension<D>,
+    Host(host): Host,
+    Extension(http): Extension<reqwest::Client>,
+    Extension(jobset): Extension<Arc<Mutex<JoinSet<()>>>>,
+    Json(data): Json<OnboardingData>,
+) -> axum::response::Response {
+    let user_uid = format!("https://{}/", host.as_str());
+
+    if db.post_exists(&user_uid).await.unwrap() {
+        IntoResponse::into_response((StatusCode::FOUND, [("Location", "/")]))
+    } else {
+        match onboard(db, user_uid.parse().unwrap(), data, http, jobset).await {
+            Ok(()) => IntoResponse::into_response((StatusCode::FOUND, [("Location", "/")])),
+            Err(err) => {
+                error!("Onboarding error: {}", err);
+                IntoResponse::into_response((
+                    err.code(),
+                    Html(
+                        Template {
+                            title: "Kittybox - Onboarding",
+                            blog_name: "Kittybox",
+                            feeds: vec![],
+                            user: None,
+                            content: ErrorPage {
+                                code: err.code(),
+                                msg: Some(err.msg().to_string()),
+                            }
+                            .to_string(),
+                        }
+                        .to_string(),
+                    ),
+                ))
+            }
+        }
+    }
+}
+
+pub fn router<S: Storage + 'static>(
+    database: S,
+    http: reqwest::Client,
+    jobset: Arc<Mutex<JoinSet<()>>>,
+) -> axum::routing::MethodRouter {
+    axum::routing::get(get)
+        .post(post::<S>)
+        .layer::<_, _, std::convert::Infallible>(axum::Extension(database))
+        .layer::<_, _, std::convert::Infallible>(axum::Extension(http))
+        .layer(axum::Extension(jobset))
+}