Skip to main content

max / makenotwork

3.4 KB · 99 lines History Blame Raw
1 //! Per-user account settings (signature + Fan+ status display).
2 //!
3 //! All routes require a logged-in session. The signature editor is gated on
4 //! [`UserPerks::effective_plus`]: non-Fan+ users see the upsell instead of an
5 //! input. Lapsed Fan+ users retain their saved markdown (we don't auto-delete
6 //! it) but the post-rendering layer hides their signature until they renew.
7
8 use axum::{
9 extract::{Form, State},
10 http::StatusCode,
11 response::{IntoResponse, Redirect, Response},
12 };
13 use tower_sessions::Session;
14
15 use crate::AppState;
16 use crate::auth::RequireUser;
17 use crate::csrf;
18 use crate::templates::AccountSettingsTemplate;
19
20 use super::{
21 SignatureForm, db_error, field_error, render_markdown, render_markdown_plus, template_user,
22 };
23
24 const SIGNATURE_MAX: usize = 1024;
25
26 #[tracing::instrument(skip_all)]
27 pub(super) async fn account_settings(
28 State(state): State<AppState>,
29 session: Session,
30 RequireUser(user): RequireUser,
31 ) -> Result<AccountSettingsTemplate, Response> {
32 let csrf_token = Some(csrf::get_or_create_token(&session).await?);
33 let (signature_markdown, signature_html) =
34 mt_db::queries::get_user_signature(&state.db, user.user_id)
35 .await
36 .map_err(db_error)?
37 .unwrap_or((None, None));
38
39 Ok(AccountSettingsTemplate {
40 csrf_token,
41 session_user: Some(template_user(&user, state.config.platform_admin_id)),
42 mnw_base_url: state.config.mnw_base_url.clone(),
43 has_plus: user.perks.effective_plus(),
44 fan_plus: user.perks.fan_plus,
45 signature_markdown,
46 signature_html,
47 })
48 }
49
50 #[tracing::instrument(skip_all)]
51 pub(super) async fn update_signature_handler(
52 State(state): State<AppState>,
53 RequireUser(user): RequireUser,
54 Form(form): Form<SignatureForm>,
55 ) -> Result<Redirect, Response> {
56 if !user.perks.effective_plus() {
57 return Err((StatusCode::FORBIDDEN, "Signatures are a Fan+ feature.").into_response());
58 }
59
60 // "Clear signature" button submits with `clear=1`; takes precedence over
61 // the textarea content.
62 if form.clear.as_deref() == Some("1") {
63 mt_db::mutations::clear_user_signature(&state.db, user.user_id)
64 .await
65 .map_err(db_error)?;
66 return Ok(Redirect::to("/account?toast=Signature+cleared"));
67 }
68
69 let trimmed = form.signature.trim();
70 if trimmed.is_empty() {
71 // Treat empty submit as a no-op rather than implicit clear, there's
72 // an explicit Clear button for that.
73 return Ok(Redirect::to("/account"));
74 }
75 if trimmed.chars().count() > SIGNATURE_MAX {
76 return Err(field_error(
77 "signature",
78 format!("Signature must be at most {SIGNATURE_MAX} characters."),
79 ));
80 }
81
82 // Render with the same plus-aware paths as posts: creators get image
83 // embeds via auto-grant, matching post-body behaviour. Render-time
84 // visibility is gated by `users.is_fan_plus` so creator signatures
85 // still only surface when they're also a Fan+ subscriber, auto-grant
86 // covers editing capability, not the public + badge / signature display.
87 let signature_html = if user.perks.effective_plus() {
88 render_markdown_plus(trimmed)
89 } else {
90 render_markdown(trimmed)
91 };
92
93 mt_db::mutations::set_user_signature(&state.db, user.user_id, trimmed, &signature_html)
94 .await
95 .map_err(db_error)?;
96
97 Ok(Redirect::to("/account?toast=Signature+saved"))
98 }
99