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 { 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, } impl OnboardingData { fn default_blog_name() -> String { "Kitty Box!".to_owned() } } #[tracing::instrument(skip(db, http))] async fn onboard( db: D, user_uid: url::Url, data: OnboardingData, http: reqwest::Client, jobset: Arc>>, ) -> 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::(&user_domain, data.blog_name.to_owned()) .await .map_err(FrontendError::from)?; db.set_setting::(&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( Extension(db): Extension, Host(host): Host, Extension(http): Extension, Extension(jobset): Extension>>>, Json(data): Json, ) -> 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( database: S, http: reqwest::Client, jobset: Arc>>, ) -> axum::routing::MethodRouter { axum::routing::get(get) .post(post::) .layer::<_, _, std::convert::Infallible>(axum::Extension(database)) .layer::<_, _, std::convert::Infallible>(axum::Extension(http)) .layer(axum::Extension(jobset)) }