Skip to main content

max / makenotwork

9.3 KB · 284 lines History Blame Raw
1 //! User-level dashboard tab handlers.
2
3 mod creator;
4 mod integrations;
5 mod payments;
6
7 pub(in crate::routes::pages::dashboard) use creator::{
8 dashboard_tab_analytics, dashboard_tab_creator,
9 };
10 pub(in crate::routes::pages::dashboard) use integrations::{
11 dashboard_tab_forums, dashboard_tab_media, dashboard_tab_synckit,
12 };
13 pub(in crate::routes::pages::dashboard) use payments::{
14 dashboard_tab_contacts, dashboard_tab_payments, dashboard_transactions,
15 };
16
17 use axum::extract::State;
18 use axum::http::HeaderMap;
19 use axum::response::IntoResponse;
20 use tower_sessions::Session;
21
22 use crate::{
23 auth::{AuthUser, SESSION_TRACKING_KEY},
24 db,
25 error::{AppError, Result},
26 helpers,
27 templates::*,
28 types::*,
29 AppState,
30 };
31
32 /// Render the HTMX partial for the dashboard settings meta-tab.
33 /// Includes profile content inline; other sections loaded via HTMX sub-nav.
34 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_settings")]
35 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_settings(
36 State(state): State<AppState>,
37 AuthUser(session_user): AuthUser,
38 ) -> Result<impl IntoResponse> {
39 let db_user = db::users::get_user_by_id(&state.db, session_user.id)
40 .await?
41 .ok_or(AppError::NotFound)?;
42
43 let db_links =
44 db::custom_links::get_custom_links_by_user(&state.db, session_user.id).await?;
45
46 let user = User::from(&db_user);
47
48 let custom_links: Vec<CustomLinkWithId> = db_links
49 .into_iter()
50 .map(|l| CustomLinkWithId {
51 id: l.id.to_string(),
52 url: l.url,
53 title: l.title,
54 })
55 .collect();
56
57 let feed_url = helpers::generate_feed_url(
58 &state.config.host_url,
59 session_user.id,
60 &state.config.signing_secret,
61 );
62
63 let custom_domain =
64 db::custom_domains::get_custom_domain_by_user(&state.db, session_user.id)
65 .await?
66 .map(|d| {
67 let instructions = if d.verified {
68 String::new()
69 } else {
70 format!(
71 "Add a DNS TXT record: _mnw-verify.{} with value {}",
72 d.domain, d.verification_token
73 )
74 };
75 crate::templates::CustomDomainInfo {
76 id: d.id.to_string(),
77 domain: d.domain,
78 verified: d.verified,
79 verification_token: d.verification_token,
80 instructions,
81 }
82 });
83
84 let has_media = session_user.can_create_projects;
85 let git_enabled = state.config.git_repos_path.is_some();
86 let has_mt_memberships = state.config.mt_base_url.is_some();
87
88 Ok(UserSettingsTabTemplate {
89 user,
90 custom_links,
91 feed_url,
92 can_create_projects: session_user.can_create_projects,
93 custom_domain,
94 has_media,
95 git_enabled,
96 has_mt_memberships,
97 })
98 }
99
100 /// Legacy route; redirects to the profile tab.
101 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_details(
102 state: State<AppState>,
103 session_user: AuthUser,
104 ) -> Result<impl IntoResponse> {
105 dashboard_tab_profile(state, session_user).await
106 }
107
108 /// Render the HTMX partial for the dashboard profile tab.
109 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_profile")]
110 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_profile(
111 State(state): State<AppState>,
112 AuthUser(session_user): AuthUser,
113 ) -> Result<impl IntoResponse> {
114 let db_user = db::users::get_user_by_id(&state.db, session_user.id)
115 .await?
116 .ok_or(AppError::NotFound)?;
117
118 let db_links =
119 db::custom_links::get_custom_links_by_user(&state.db, session_user.id).await?;
120
121 let user = User::from(&db_user);
122
123 let custom_links: Vec<CustomLinkWithId> = db_links
124 .into_iter()
125 .map(|l| CustomLinkWithId {
126 id: l.id.to_string(),
127 url: l.url,
128 title: l.title,
129 })
130 .collect();
131
132 let feed_url = helpers::generate_feed_url(
133 &state.config.host_url,
134 session_user.id,
135 &state.config.signing_secret,
136 );
137
138 let custom_domain =
139 db::custom_domains::get_custom_domain_by_user(&state.db, session_user.id)
140 .await?
141 .map(|d| {
142 let instructions = if d.verified {
143 String::new()
144 } else {
145 format!(
146 "Add a DNS TXT record: _mnw-verify.{} with value {}",
147 d.domain, d.verification_token
148 )
149 };
150 crate::templates::CustomDomainInfo {
151 id: d.id.to_string(),
152 domain: d.domain,
153 verified: d.verified,
154 verification_token: d.verification_token,
155 instructions,
156 }
157 });
158
159 Ok(UserProfileTabTemplate {
160 user,
161 custom_links,
162 feed_url,
163 can_create_projects: session_user.can_create_projects,
164 custom_domain,
165 })
166 }
167
168 /// Render the HTMX partial for the dashboard account tab.
169 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_account")]
170 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_account(
171 State(state): State<AppState>,
172 session: Session,
173 AuthUser(session_user): AuthUser,
174 ) -> Result<impl IntoResponse> {
175 let db_user = db::users::get_user_by_id(&state.db, session_user.id)
176 .await?
177 .ok_or(AppError::NotFound)?;
178
179 let user = User::from(&db_user);
180
181 let sessions =
182 db::sessions::get_user_sessions(&state.db, session_user.id).await?;
183 let current_session_id = session
184 .get::<db::UserSessionId>(SESSION_TRACKING_KEY)
185 .await
186 .ok()
187 .flatten();
188
189 // Fetch moderation actions for "Account Status" section
190 let active_actions = db::moderation::get_active_actions(&state.db, session_user.id).await?;
191 let all_actions = db::moderation::get_history(&state.db, session_user.id).await?;
192
193 let moderation_active: Vec<ModerationActionView> = active_actions
194 .iter()
195 .map(|a| ModerationActionView {
196 action_label: a.action_type.label().to_string(),
197 reason: a.reason.clone(),
198 created_at: a.created_at.format("%b %-d, %Y").to_string(),
199 resolved_at: None,
200 })
201 .collect();
202
203 let moderation_history: Vec<ModerationActionView> = all_actions
204 .iter()
205 .filter(|a| a.resolved_at.is_some())
206 .map(|a| ModerationActionView {
207 action_label: a.action_type.label().to_string(),
208 reason: a.reason.clone(),
209 created_at: a.created_at.format("%b %-d, %Y").to_string(),
210 resolved_at: a.resolved_at.map(|d| d.format("%b %-d, %Y").to_string()),
211 })
212 .collect();
213
214 let fan_plus = db::fan_plus::get_fan_plus_by_user(&state.db, session_user.id)
215 .await?
216 .filter(|sub| matches!(sub.status, db::SubscriptionStatus::Active | db::SubscriptionStatus::PastDue))
217 .map(|sub| crate::templates::FanPlusPaneView {
218 period_end: sub.current_period_end.map(|d| d.format("%b %-d, %Y").to_string()),
219 cancel_at_period_end: sub.cancel_at_period_end,
220 });
221
222 let csrf_token = crate::csrf::get_or_create_token(&session).await.ok();
223
224 Ok(UserAccountTabTemplate {
225 user,
226 sessions,
227 current_session_id,
228 can_create_projects: session_user.can_create_projects,
229 email_verified: db_user.email_verified,
230 moderation_active,
231 moderation_history,
232 creator_paused: db_user.is_creator_paused(),
233 fan_plus,
234 csrf_token,
235 })
236 }
237
238 /// Render the HTMX partial for the dashboard projects tab.
239 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_projects")]
240 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_projects(
241 State(state): State<AppState>,
242 AuthUser(session_user): AuthUser,
243 headers: HeaderMap,
244 ) -> Result<axum::response::Response> {
245 let generation =
246 db::users::get_cache_generation(&state.db, session_user.id).await?;
247 if let Some(not_modified) = helpers::check_etag(&headers, generation) {
248 return Ok(not_modified);
249 }
250
251 let db_projects =
252 db::projects::get_projects_by_user(&state.db, session_user.id).await?;
253
254 let projects: Vec<ProjectCard> =
255 db_projects.iter().map(ProjectCard::from_db).collect();
256
257 Ok(helpers::with_etag(
258 generation,
259 UserProjectsTabTemplate {
260 projects,
261 can_create_projects: session_user.can_create_projects,
262 },
263 ))
264 }
265
266 /// Support tab; submit a support ticket.
267 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_support")]
268 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_support(
269 AuthUser(session_user): AuthUser,
270 ) -> Result<impl IntoResponse> {
271 Ok(UserSupportTabTemplate {
272 email: session_user.email.clone(),
273 })
274 }
275
276 /// SSH Keys tab; manage SSH keys for git access.
277 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_ssh_keys")]
278 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_ssh_keys(
279 AuthUser(session_user): AuthUser,
280 ) -> Result<impl IntoResponse> {
281 let username = session_user.username.to_string();
282 Ok(UserSshKeysTabTemplate { username })
283 }
284