Skip to main content

max / makenotwork

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