Skip to main content

max / makenotwork

4.4 KB · 135 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::auth::MaybeUser;
16 use crate::csrf;
17 use crate::templates::*;
18 use crate::AppState;
19
20 use super::{
21 render_markdown, render_markdown_plus, template_user, SignatureForm,
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 MaybeUser(session_user): MaybeUser,
31 ) -> Result<AccountSettingsTemplate, Response> {
32 let csrf_token = Some(csrf::get_or_create_token(&session).await);
33 let user = session_user
34 .ok_or_else(|| Redirect::to("/auth/login").into_response())?;
35
36 let row: Option<(Option<String>, Option<String>)> = sqlx::query_as(
37 "SELECT signature_markdown, signature_html FROM users WHERE mnw_account_id = $1",
38 )
39 .bind(user.user_id)
40 .fetch_optional(&state.db)
41 .await
42 .map_err(|e| {
43 tracing::error!(error = ?e, "db error loading signature");
44 StatusCode::INTERNAL_SERVER_ERROR.into_response()
45 })?;
46 let (signature_markdown, signature_html) = row.unwrap_or((None, None));
47
48 Ok(AccountSettingsTemplate {
49 csrf_token,
50 session_user: Some(template_user(&user, state.config.platform_admin_id)),
51 mnw_base_url: state.config.mnw_base_url.clone(),
52 has_plus: user.perks.effective_plus(),
53 fan_plus: user.perks.fan_plus,
54 signature_markdown,
55 signature_html,
56 error: None,
57 })
58 }
59
60 #[tracing::instrument(skip_all)]
61 pub(super) async fn update_signature_handler(
62 State(state): State<AppState>,
63 MaybeUser(session_user): MaybeUser,
64 Form(form): Form<SignatureForm>,
65 ) -> Result<Redirect, Response> {
66 let user = session_user
67 .ok_or_else(|| Redirect::to("/auth/login").into_response())?;
68
69 if !user.perks.effective_plus() {
70 return Err((
71 StatusCode::FORBIDDEN,
72 "Signatures are a Fan+ feature.",
73 )
74 .into_response());
75 }
76
77 // "Clear signature" button submits with `clear=1`; takes precedence over
78 // the textarea content.
79 if form.clear.as_deref() == Some("1") {
80 sqlx::query(
81 "UPDATE users SET signature_markdown = NULL, signature_html = NULL \
82 WHERE mnw_account_id = $1",
83 )
84 .bind(user.user_id)
85 .execute(&state.db)
86 .await
87 .map_err(|e| {
88 tracing::error!(error = ?e, "db error clearing signature");
89 StatusCode::INTERNAL_SERVER_ERROR.into_response()
90 })?;
91 return Ok(Redirect::to("/account?toast=Signature+cleared"));
92 }
93
94 let trimmed = form.signature.trim();
95 if trimmed.is_empty() {
96 // Treat empty submit as a no-op rather than implicit clear — there's
97 // an explicit Clear button for that.
98 return Ok(Redirect::to("/account"));
99 }
100 if trimmed.chars().count() > SIGNATURE_MAX {
101 return Err((
102 StatusCode::UNPROCESSABLE_ENTITY,
103 format!("Signature must be at most {SIGNATURE_MAX} characters."),
104 )
105 .into_response());
106 }
107
108 // Render with the same plus-aware paths as posts: creators get image
109 // embeds via auto-grant, matching post-body behaviour. Render-time
110 // visibility is gated by `users.is_fan_plus` so creator signatures
111 // still only surface when they're also a Fan+ subscriber — auto-grant
112 // covers editing capability, not the public + badge / signature display.
113 let signature_html = if user.perks.effective_plus() {
114 render_markdown_plus(trimmed)
115 } else {
116 render_markdown(trimmed)
117 };
118
119 sqlx::query(
120 "UPDATE users SET signature_markdown = $2, signature_html = $3 \
121 WHERE mnw_account_id = $1",
122 )
123 .bind(user.user_id)
124 .bind(trimmed)
125 .bind(&signature_html)
126 .execute(&state.db)
127 .await
128 .map_err(|e| {
129 tracing::error!(error = ?e, "db error saving signature");
130 StatusCode::INTERNAL_SERVER_ERROR.into_response()
131 })?;
132
133 Ok(Redirect::to("/account?toast=Signature+saved"))
134 }
135