//! Per-user account settings (signature + Fan+ status display). //! //! All routes require a logged-in session. The signature editor is gated on //! [`UserPerks::effective_plus`]: non-Fan+ users see the upsell instead of an //! input. Lapsed Fan+ users retain their saved markdown (we don't auto-delete //! it) but the post-rendering layer hides their signature until they renew. use axum::{ extract::{Form, State}, http::StatusCode, response::{IntoResponse, Redirect, Response}, }; use tower_sessions::Session; use crate::AppState; use crate::auth::RequireUser; use crate::csrf; use crate::templates::AccountSettingsTemplate; use super::{ SignatureForm, db_error, field_error, render_markdown, render_markdown_plus, template_user, }; const SIGNATURE_MAX: usize = 1024; #[tracing::instrument(skip_all)] pub(super) async fn account_settings( State(state): State, session: Session, RequireUser(user): RequireUser, ) -> Result { let csrf_token = Some(csrf::get_or_create_token(&session).await?); let (signature_markdown, signature_html) = mt_db::queries::get_user_signature(&state.db, user.user_id) .await .map_err(db_error)? .unwrap_or((None, None)); Ok(AccountSettingsTemplate { csrf_token, session_user: Some(template_user(&user, state.config.platform_admin_id)), mnw_base_url: state.config.mnw_base_url.clone(), has_plus: user.perks.effective_plus(), fan_plus: user.perks.fan_plus, signature_markdown, signature_html, }) } #[tracing::instrument(skip_all)] pub(super) async fn update_signature_handler( State(state): State, RequireUser(user): RequireUser, Form(form): Form, ) -> Result { if !user.perks.effective_plus() { return Err((StatusCode::FORBIDDEN, "Signatures are a Fan+ feature.").into_response()); } // "Clear signature" button submits with `clear=1`; takes precedence over // the textarea content. if form.clear.as_deref() == Some("1") { mt_db::mutations::clear_user_signature(&state.db, user.user_id) .await .map_err(db_error)?; return Ok(Redirect::to("/account?toast=Signature+cleared")); } let trimmed = form.signature.trim(); if trimmed.is_empty() { // Treat empty submit as a no-op rather than implicit clear, there's // an explicit Clear button for that. return Ok(Redirect::to("/account")); } if trimmed.chars().count() > SIGNATURE_MAX { return Err(field_error( "signature", format!("Signature must be at most {SIGNATURE_MAX} characters."), )); } // Render with the same plus-aware paths as posts: creators get image // embeds via auto-grant, matching post-body behaviour. Render-time // visibility is gated by `users.is_fan_plus` so creator signatures // still only surface when they're also a Fan+ subscriber, auto-grant // covers editing capability, not the public + badge / signature display. let signature_html = if user.perks.effective_plus() { render_markdown_plus(trimmed) } else { render_markdown(trimmed) }; mt_db::mutations::set_user_signature(&state.db, user.user_id, trimmed, &signature_html) .await .map_err(db_error)?; Ok(Redirect::to("/account?toast=Signature+saved")) }