Skip to main content

max / makenotwork

6.8 KB · 209 lines History Blame Raw
1 //! Inline edit forms, export, and account deletion pages.
2
3 use crate::extractors::ValidatedQuery;
4 use axum::extract::{Path, State};
5 use axum::response::IntoResponse;
6 use serde::Deserialize;
7 use tower_sessions::Session;
8
9 use crate::{
10 auth::AuthUser,
11 db::{self, BlogPostId, ItemId, Slug},
12 error::{AppError, Result},
13 helpers::get_csrf_token,
14 templates::{
15 BlogEditorTemplate, DeleteAccountTemplate, ExportPortalTemplate, ImportPortalTemplate,
16 ItemEditRowTemplate,
17 },
18 types::ContentItem,
19 };
20 use sqlx::PgPool;
21
22 /// Render an inline edit row for an item in the project content table.
23 #[tracing::instrument(skip_all, name = "dashboard_forms::item_edit_row")]
24 pub(super) async fn item_edit_row(
25 State(db): State<PgPool>,
26 AuthUser(session_user): AuthUser,
27 Path(id): Path<String>,
28 ) -> Result<impl IntoResponse> {
29 let item_id: ItemId = id.parse().map_err(|_| AppError::NotFound)?;
30
31 let db_item = db::items::get_item_by_id(&db, item_id)
32 .await?
33 .ok_or(AppError::NotFound)?;
34
35 let db_project = db::projects::get_project_by_id(&db, db_item.project_id)
36 .await?
37 .ok_or(AppError::NotFound)?;
38
39 if db_project.user_id != session_user.id {
40 return Err(AppError::Forbidden);
41 }
42
43 let item = ContentItem::from_db(&db_item, 0, session_user.settlement_currency);
44
45 Ok(ItemEditRowTemplate { item })
46 }
47
48 /// Render the data export portal page.
49 #[tracing::instrument(skip_all, name = "dashboard_forms::export_portal")]
50 pub(super) async fn export_portal(
51 State(db): State<PgPool>,
52 session: Session,
53 AuthUser(session_user): AuthUser,
54 ) -> Result<impl IntoResponse> {
55 let csrf_token = get_csrf_token(&session).await;
56
57 // Check if user has any S3 content (items, versions, insertions)
58 let items = db::items::get_items_by_user(&db, session_user.id).await?;
59 let has_item_content = items.iter().any(crate::db::DbItem::has_s3_content);
60
61 // Sum known file sizes from versions and insertions
62 let known_size = db::creator_tiers::get_user_content_size(&db, session_user.id).await?;
63 let has_content = has_item_content || known_size > 0;
64
65 let content_size = if !has_content {
66 "No files".to_string()
67 } else if has_item_content && known_size > 0 {
68 format!(
69 "{} + audio/cover files",
70 crate::helpers::format_file_size(known_size)
71 )
72 } else if known_size > 0 {
73 crate::helpers::format_file_size(known_size)
74 } else {
75 "Audio/cover files".to_string()
76 };
77
78 Ok(ExportPortalTemplate {
79 csrf_token,
80 session_user: Some(session_user),
81 has_content,
82 content_size,
83 })
84 }
85
86 /// Render the data import portal page.
87 #[tracing::instrument(skip_all, name = "dashboard_forms::import_portal")]
88 pub(super) async fn import_portal(
89 State(db): State<PgPool>,
90 session: Session,
91 AuthUser(session_user): AuthUser,
92 ) -> Result<impl IntoResponse> {
93 let csrf_token = get_csrf_token(&session).await;
94
95 let db_projects = db::projects::get_projects_by_user(&db, session_user.id).await?;
96 let projects: Vec<crate::templates::ImportProjectOption> = db_projects
97 .into_iter()
98 .map(|p| crate::templates::ImportProjectOption {
99 id: p.id.to_string(),
100 title: p.title,
101 })
102 .collect();
103
104 let db_jobs = db::imports::list_import_jobs(&db, session_user.id).await?;
105 let jobs: Vec<crate::templates::ImportJobRow> = db_jobs
106 .into_iter()
107 .map(|j| crate::templates::ImportJobRow {
108 source: j.source.to_string(),
109 status: j.status.to_string(),
110 status_tone: j.status.badge_status().tone(),
111 total_rows: j.total_rows,
112 created_rows: j.created_rows,
113 created_at: j.created_at,
114 })
115 .collect();
116
117 Ok(ImportPortalTemplate {
118 csrf_token,
119 session_user: Some(session_user),
120 projects,
121 jobs,
122 })
123 }
124
125 /// Render the account deletion confirmation page.
126 #[tracing::instrument(skip_all, name = "dashboard_forms::delete_account_page")]
127 pub(super) async fn delete_account_page(
128 session: Session,
129 AuthUser(session_user): AuthUser,
130 ) -> Result<impl IntoResponse> {
131 let csrf_token = get_csrf_token(&session).await;
132
133 Ok(DeleteAccountTemplate {
134 csrf_token,
135 session_user: Some(session_user.clone()),
136 username: session_user.username.to_string(),
137 })
138 }
139
140 /// Query parameters for the blog editor page.
141 #[derive(Deserialize)]
142 pub(super) struct BlogEditorQuery {
143 pub post: Option<String>,
144 }
145
146 /// Render the blog post editor page (create new or edit existing).
147 #[tracing::instrument(skip_all, name = "dashboard_forms::blog_editor")]
148 pub(super) async fn blog_editor(
149 State(db): State<PgPool>,
150 session: Session,
151 AuthUser(session_user): AuthUser,
152 Path(slug): Path<String>,
153 ValidatedQuery(query): ValidatedQuery<BlogEditorQuery>,
154 ) -> Result<impl IntoResponse> {
155 let slug_val = Slug::new(&slug).map_err(|_| AppError::NotFound)?;
156 let db_project = db::projects::get_project_by_user_and_slug(&db, session_user.id, &slug_val)
157 .await?
158 .ok_or(AppError::NotFound)?;
159
160 let csrf_token = get_csrf_token(&session).await;
161
162 // The landing "Show on landing" control only applies to the platform
163 // changelog project; surfacing it elsewhere would expose an inert flag.
164 let is_changelog_project = db_project.slug.as_str() == crate::constants::CHANGELOG_PROJECT_SLUG;
165
166 // If editing an existing post, load it
167 if let Some(post_id_str) = &query.post {
168 let post_id: BlogPostId = post_id_str.parse().map_err(|_| AppError::NotFound)?;
169 let post = db::blog_posts::get_blog_post_by_id(&db, post_id)
170 .await?
171 .ok_or(AppError::NotFound)?;
172
173 if post.project_id != db_project.id {
174 return Err(AppError::NotFound);
175 }
176
177 return Ok(BlogEditorTemplate {
178 csrf_token,
179 session_user: Some(session_user),
180 project_id: db_project.id.to_string(),
181 project_slug: db_project.slug.to_string(),
182 editing: true,
183 post_id: post.id.to_string(),
184 post_title: post.title,
185 post_slug: post.slug.to_string(),
186 post_body: post.body_markdown,
187 post_is_published: post.published_at.is_some(),
188 is_changelog_project,
189 post_show_on_landing: post.show_on_landing,
190 });
191 }
192
193 // New post
194 Ok(BlogEditorTemplate {
195 csrf_token,
196 session_user: Some(session_user),
197 project_id: db_project.id.to_string(),
198 project_slug: db_project.slug.to_string(),
199 editing: false,
200 post_id: String::new(),
201 post_title: String::new(),
202 post_slug: String::new(),
203 post_body: String::new(),
204 post_is_published: false,
205 is_changelog_project,
206 post_show_on_landing: false,
207 })
208 }
209