Skip to main content

max / makenotwork

5.5 KB · 170 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, ImportPortalTemplate, ItemEditRowTemplate,
16 },
17 types::ContentItem,
18 };
19 use sqlx::PgPool;
20
21 /// Render an inline edit row for an item in the project content table.
22 #[tracing::instrument(skip_all, name = "dashboard_forms::item_edit_row")]
23 pub(super) async fn item_edit_row(
24 State(db): State<PgPool>,
25 AuthUser(session_user): AuthUser,
26 Path(id): Path<String>,
27 ) -> Result<impl IntoResponse> {
28 let item_id: ItemId = id.parse().map_err(|_| AppError::NotFound)?;
29
30 let db_item = db::items::get_item_by_id(&db, item_id)
31 .await?
32 .ok_or(AppError::NotFound)?;
33
34 let db_project = db::projects::get_project_by_id(&db, db_item.project_id)
35 .await?
36 .ok_or(AppError::NotFound)?;
37
38 if db_project.user_id != session_user.id {
39 return Err(AppError::Forbidden);
40 }
41
42 let item = ContentItem::from_db(&db_item, 0, session_user.settlement_currency);
43
44 Ok(ItemEditRowTemplate { item })
45 }
46
47 /// Render the data import portal page.
48 #[tracing::instrument(skip_all, name = "dashboard_forms::import_portal")]
49 pub(super) async fn import_portal(
50 State(db): State<PgPool>,
51 session: Session,
52 AuthUser(session_user): AuthUser,
53 ) -> Result<impl IntoResponse> {
54 let csrf_token = get_csrf_token(&session).await;
55
56 let db_projects = db::projects::get_projects_by_user(&db, session_user.id).await?;
57 let projects: Vec<crate::templates::ImportProjectOption> = db_projects
58 .into_iter()
59 .map(|p| crate::templates::ImportProjectOption {
60 id: p.id.to_string(),
61 title: p.title,
62 })
63 .collect();
64
65 let db_jobs = db::imports::list_import_jobs(&db, session_user.id).await?;
66 let jobs: Vec<crate::templates::ImportJobRow> = db_jobs
67 .into_iter()
68 .map(|j| crate::templates::ImportJobRow {
69 source: j.source.to_string(),
70 status: j.status.to_string(),
71 status_tone: j.status.badge_status().tone(),
72 total_rows: j.total_rows,
73 created_rows: j.created_rows,
74 created_at: j.created_at,
75 })
76 .collect();
77
78 Ok(ImportPortalTemplate {
79 csrf_token,
80 session_user: Some(session_user),
81 projects,
82 jobs,
83 })
84 }
85
86 /// Render the account deletion confirmation page.
87 #[tracing::instrument(skip_all, name = "dashboard_forms::delete_account_page")]
88 pub(super) async fn delete_account_page(
89 session: Session,
90 AuthUser(session_user): AuthUser,
91 ) -> Result<impl IntoResponse> {
92 let csrf_token = get_csrf_token(&session).await;
93
94 Ok(DeleteAccountTemplate {
95 csrf_token,
96 session_user: Some(session_user.clone()),
97 username: session_user.username.to_string(),
98 })
99 }
100
101 /// Query parameters for the blog editor page.
102 #[derive(Deserialize)]
103 pub(super) struct BlogEditorQuery {
104 pub post: Option<String>,
105 }
106
107 /// Render the blog post editor page (create new or edit existing).
108 #[tracing::instrument(skip_all, name = "dashboard_forms::blog_editor")]
109 pub(super) async fn blog_editor(
110 State(db): State<PgPool>,
111 session: Session,
112 AuthUser(session_user): AuthUser,
113 Path(slug): Path<String>,
114 ValidatedQuery(query): ValidatedQuery<BlogEditorQuery>,
115 ) -> Result<impl IntoResponse> {
116 let slug_val = Slug::new(&slug).map_err(|_| AppError::NotFound)?;
117 let db_project = db::projects::get_project_by_user_and_slug(&db, session_user.id, &slug_val)
118 .await?
119 .ok_or(AppError::NotFound)?;
120
121 let csrf_token = get_csrf_token(&session).await;
122
123 // The landing "Show on landing" control only applies to the platform
124 // changelog project; surfacing it elsewhere would expose an inert flag.
125 let is_changelog_project = db_project.slug.as_str() == crate::constants::CHANGELOG_PROJECT_SLUG;
126
127 // If editing an existing post, load it
128 if let Some(post_id_str) = &query.post {
129 let post_id: BlogPostId = post_id_str.parse().map_err(|_| AppError::NotFound)?;
130 let post = db::blog_posts::get_blog_post_by_id(&db, post_id)
131 .await?
132 .ok_or(AppError::NotFound)?;
133
134 if post.project_id != db_project.id {
135 return Err(AppError::NotFound);
136 }
137
138 return Ok(BlogEditorTemplate {
139 csrf_token,
140 session_user: Some(session_user),
141 project_id: db_project.id.to_string(),
142 project_slug: db_project.slug.to_string(),
143 editing: true,
144 post_id: post.id.to_string(),
145 post_title: post.title,
146 post_slug: post.slug.to_string(),
147 post_body: post.body_markdown,
148 post_is_published: post.published_at.is_some(),
149 is_changelog_project,
150 post_show_on_landing: post.show_on_landing,
151 });
152 }
153
154 // New post
155 Ok(BlogEditorTemplate {
156 csrf_token,
157 session_user: Some(session_user),
158 project_id: db_project.id.to_string(),
159 project_slug: db_project.slug.to_string(),
160 editing: false,
161 post_id: String::new(),
162 post_title: String::new(),
163 post_slug: String::new(),
164 post_body: String::new(),
165 post_is_published: false,
166 is_changelog_project,
167 post_show_on_landing: false,
168 })
169 }
170