//! Inline edit forms, export, and account deletion pages. use axum::extract::{Path, Query, State}; use axum::response::IntoResponse; use serde::Deserialize; use tower_sessions::Session; use crate::{ auth::AuthUser, db::{self, BlogPostId, ItemId, Slug}, error::{AppError, Result}, helpers::get_csrf_token, templates::*, types::*, AppState, }; /// Render an inline edit row for an item in the project content table. #[tracing::instrument(skip_all, name = "dashboard_forms::item_edit_row")] pub(super) async fn item_edit_row( State(state): State, AuthUser(session_user): AuthUser, Path(id): Path, ) -> Result { let item_id: ItemId = id.parse().map_err(|_| AppError::NotFound)?; let db_item = db::items::get_item_by_id(&state.db, item_id) .await? .ok_or(AppError::NotFound)?; let db_project = db::projects::get_project_by_id(&state.db, db_item.project_id) .await? .ok_or(AppError::NotFound)?; if db_project.user_id != session_user.id { return Err(AppError::Forbidden); } let item = ContentItem::from_db(&db_item, 0); Ok(ItemEditRowTemplate { item }) } /// Render the data export portal page. #[tracing::instrument(skip_all, name = "dashboard_forms::export_portal")] pub(super) async fn export_portal( State(state): State, session: Session, AuthUser(session_user): AuthUser, ) -> Result { let csrf_token = get_csrf_token(&session).await; // Check if user has any S3 content (items, versions, insertions) let items = db::items::get_items_by_user(&state.db, session_user.id).await?; let has_item_content = items.iter().any(|i| i.has_s3_content()); // Sum known file sizes from versions and insertions let known_size = db::creator_tiers::get_user_content_size(&state.db, session_user.id).await?; let has_content = has_item_content || known_size > 0; let content_size = if !has_content { "No files".to_string() } else if has_item_content && known_size > 0 { format!("{} + audio/cover files", crate::helpers::format_file_size(known_size)) } else if known_size > 0 { crate::helpers::format_file_size(known_size) } else { "Audio/cover files".to_string() }; Ok(ExportPortalTemplate { csrf_token, session_user: Some(session_user), has_content, content_size, }) } /// Render the data import portal page. #[tracing::instrument(skip_all, name = "dashboard_forms::import_portal")] pub(super) async fn import_portal( State(state): State, session: Session, AuthUser(session_user): AuthUser, ) -> Result { let csrf_token = get_csrf_token(&session).await; let db_projects = db::projects::get_projects_by_user(&state.db, session_user.id).await?; let projects: Vec = db_projects .into_iter() .map(|p| crate::templates::ImportProjectOption { id: p.id.to_string(), title: p.title, }) .collect(); let db_jobs = db::imports::list_import_jobs(&state.db, session_user.id).await?; let jobs: Vec = db_jobs .into_iter() .map(|j| crate::templates::ImportJobRow { source: j.source.to_string(), status: j.status.to_string(), total_rows: j.total_rows, created_rows: j.created_rows, created_at: j.created_at, }) .collect(); Ok(ImportPortalTemplate { csrf_token, session_user: Some(session_user), projects, jobs, }) } /// Render the account deletion confirmation page. #[tracing::instrument(skip_all, name = "dashboard_forms::delete_account_page")] pub(super) async fn delete_account_page( State(_state): State, session: Session, AuthUser(session_user): AuthUser, ) -> Result { let csrf_token = get_csrf_token(&session).await; Ok(DeleteAccountTemplate { csrf_token, session_user: Some(session_user.clone()), username: session_user.username.to_string(), }) } /// Query parameters for the blog editor page. #[derive(Deserialize)] pub(super) struct BlogEditorQuery { pub post: Option, } /// Render the blog post editor page (create new or edit existing). #[tracing::instrument(skip_all, name = "dashboard_forms::blog_editor")] pub(super) async fn blog_editor( State(state): State, session: Session, AuthUser(session_user): AuthUser, Path(slug): Path, Query(query): Query, ) -> Result { let slug_val = Slug::new(&slug).map_err(|_| AppError::NotFound)?; let db_project = db::projects::get_project_by_user_and_slug(&state.db, session_user.id, &slug_val) .await? .ok_or(AppError::NotFound)?; let csrf_token = get_csrf_token(&session).await; // The landing "Show on landing" control only applies to the platform // changelog project; surfacing it elsewhere would expose an inert flag. let is_changelog_project = db_project.slug.as_str() == crate::constants::CHANGELOG_PROJECT_SLUG; // If editing an existing post, load it if let Some(post_id_str) = &query.post { let post_id: BlogPostId = post_id_str.parse().map_err(|_| AppError::NotFound)?; let post = db::blog_posts::get_blog_post_by_id(&state.db, post_id) .await? .ok_or(AppError::NotFound)?; if post.project_id != db_project.id { return Err(AppError::NotFound); } return Ok(BlogEditorTemplate { csrf_token, session_user: Some(session_user), project_id: db_project.id.to_string(), project_slug: db_project.slug.to_string(), editing: true, post_id: post.id.to_string(), post_title: post.title, post_slug: post.slug.to_string(), post_body: post.body_markdown, post_is_published: post.published_at.is_some(), is_changelog_project, post_show_on_landing: post.show_on_landing, }); } // New post Ok(BlogEditorTemplate { csrf_token, session_user: Some(session_user), project_id: db_project.id.to_string(), project_slug: db_project.slug.to_string(), editing: false, post_id: String::new(), post_title: String::new(), post_slug: String::new(), post_body: String::new(), post_is_published: false, is_changelog_project, post_show_on_landing: false, }) }