Skip to main content

max / makenotwork

3.9 KB · 142 lines History Blame Raw
1 //! Support ticket submission from the user dashboard.
2
3 use axum::{
4 Form,
5 extract::State,
6 response::{Html, IntoResponse, Response},
7 };
8 use serde::Deserialize;
9
10 use crate::{
11 Integrations, auth::AuthUser, email::EmailClient, error::Result, templates::FormStatusTemplate,
12 };
13
14 /// Form input for submitting a support ticket.
15 #[derive(Debug, Deserialize)]
16 pub(crate) struct SupportTicketForm {
17 pub subject: String,
18 pub message: String,
19 pub category: String,
20 }
21
22 /// Submit a support ticket. Creates a WAM ticket and sends a confirmation email.
23 #[tracing::instrument(skip_all, name = "users::submit_support_ticket")]
24 pub(in crate::routes::api) async fn submit_support_ticket(
25 State(email): State<EmailClient>,
26 State(integrations): State<Integrations>,
27 AuthUser(user): AuthUser,
28 Form(form): Form<SupportTicketForm>,
29 ) -> Result<Response> {
30 user.check_not_sandbox()?;
31 let subject = form.subject.trim();
32 let message = form.message.trim();
33 let category = form.category.trim();
34
35 if subject.is_empty() || subject.chars().count() > 200 {
36 return Ok(Html(
37 FormStatusTemplate {
38 success: false,
39 message: "Subject must be between 1 and 200 characters.".to_string(),
40 }
41 .render_string()?,
42 )
43 .into_response());
44 }
45
46 if message.is_empty() || message.chars().count() > 5000 {
47 return Ok(Html(
48 FormStatusTemplate {
49 success: false,
50 message: "Message must be between 1 and 5000 characters.".to_string(),
51 }
52 .render_string()?,
53 )
54 .into_response());
55 }
56
57 let valid_categories = ["bug", "billing", "account", "content", "security", "other"];
58 if !valid_categories.contains(&category) {
59 return Ok(Html(
60 FormStatusTemplate {
61 success: false,
62 message: "Please select a valid category.".to_string(),
63 }
64 .render_string()?,
65 )
66 .into_response());
67 }
68
69 // Build ticket body
70 let body = format!(
71 "From: {} ({})\nCategory: {}\n\n{}",
72 user.username, user.email, category, message
73 );
74
75 // Create WAM ticket for triage
76 if let Some(ref wam) = integrations.wam {
77 let title = format!("[support] {subject}");
78 wam.create_ticket(
79 &title,
80 Some(&body),
81 "medium",
82 "support-ticket",
83 Some(&user.id.to_string()),
84 )
85 .await;
86 }
87
88 // Send confirmation email to the user
89 let confirmation = format!(
90 r"Hi {},
91
92 We received your support request:
93
94 Subject: {}
95 Category: {}
96
97 {}
98
99 We'll get back to you as soon as we can. Every response is from a real person.
100
101 --
102 Makenotwork Support",
103 user.display_name
104 .as_deref()
105 .unwrap_or(user.username.as_ref()),
106 subject,
107 category,
108 message,
109 );
110 if let Err(e) = email
111 .send_alert(
112 &user.email,
113 &format!("We received your request: {subject}"),
114 &confirmation,
115 )
116 .await
117 {
118 tracing::warn!(error = ?e, "failed to send support confirmation email");
119 }
120
121 // Send notification email to support
122 let support_body = format!(
123 "New support ticket from {} ({}):\n\nSubject: {}\nCategory: {}\nUser ID: {}\n\n{}",
124 user.username, user.email, subject, category, user.id, message,
125 );
126 if let Err(e) = email
127 .send_alert(
128 "info@makenot.work",
129 &format!("[support] {subject}"),
130 &support_body,
131 )
132 .await
133 {
134 tracing::warn!(error = ?e, "failed to send support notification email");
135 }
136
137 Ok(Html(FormStatusTemplate {
138 success: true,
139 message: "Your message has been submitted. Check your email for a confirmation. We'll get back to you soon.".to_string(),
140 }.render_string()?).into_response())
141 }
142