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 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,
) -> Result<(), FrontendError> {
// Create a user to pass to the backend
// At this point the site belongs to nobody, so it is safe to do
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
};
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",
));
}
db.set_setting::<settings::SiteName>(user.me.as_str(), data.blog_name.to_owned())
.await
.map_err(FrontendError::from)?;
db.set_setting::<settings::Webring>(user.me.as_str(), 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_uid.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);
crate::micropub::_post(&user, uid, post, db, http)
.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,
Json(data): Json<OnboardingData>,
Extension(http): Extension<reqwest::Client>,
) -> 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).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) -> axum::routing::MethodRouter {
axum::routing::get(get)
.post(post::<S>)
.layer(axum::Extension(database))
.layer(axum::Extension(http))
}