Skip to main content

max / makenotwork

3.4 KB · 119 lines History Blame Raw
1 //! User-facing report submission endpoint.
2
3 use axum::{Form, extract::State, response::IntoResponse};
4 use serde::Deserialize;
5 use uuid::Uuid;
6
7 use sqlx::PgPool;
8
9 use crate::{
10 auth::AuthUser,
11 db::{self, ReportTargetType, ReportType},
12 error::{AppError, Result},
13 templates::AlertTemplate,
14 };
15
16 /// Form input for submitting a report.
17 #[derive(Debug, Deserialize)]
18 pub(super) struct ReportForm {
19 pub target_type: String,
20 pub target_id: String,
21 pub report_type: String,
22 #[serde(default)]
23 pub reason: String,
24 }
25
26 /// Submit a report (logged-in users only).
27 #[tracing::instrument(skip_all, name = "api::submit_report")]
28 pub(super) async fn submit_report(
29 State(db): State<PgPool>,
30 AuthUser(user): AuthUser,
31 Form(form): Form<ReportForm>,
32 ) -> Result<impl IntoResponse> {
33 user.check_not_sandbox()?;
34 // Parse target_type
35 let target_type: ReportTargetType = form
36 .target_type
37 .parse()
38 .map_err(|_| AppError::validation("Invalid target type".to_string()))?;
39
40 // Parse target_id
41 let target_id: Uuid = form
42 .target_id
43 .parse()
44 .map_err(|_| AppError::validation("Invalid target ID".to_string()))?;
45
46 // Parse report_type
47 let report_type: ReportType = form
48 .report_type
49 .parse()
50 .map_err(|_| AppError::validation("Invalid report type".to_string()))?;
51
52 // Require reason for "other" type
53 let reason = form.reason.trim();
54 if report_type == ReportType::Other && reason.is_empty() {
55 return Err(AppError::validation(
56 "Please provide details for 'Other' reports".to_string(),
57 ));
58 }
59
60 // Prevent self-reporting
61 match target_type {
62 ReportTargetType::Project => {
63 let project = db::projects::get_project_by_id(&db, target_id.into())
64 .await?
65 .ok_or(AppError::NotFound)?;
66 if project.user_id == user.id {
67 return Err(AppError::validation(
68 "You cannot report your own project".to_string(),
69 ));
70 }
71 }
72 ReportTargetType::Item => {
73 let item = db::items::get_item_by_id(&db, target_id.into())
74 .await?
75 .ok_or(AppError::NotFound)?;
76 let project = db::projects::get_project_by_id(&db, item.project_id)
77 .await?
78 .ok_or(AppError::NotFound)?;
79 if project.user_id == user.id {
80 return Err(AppError::validation(
81 "You cannot report your own item".to_string(),
82 ));
83 }
84 }
85 }
86
87 // Rate limit: max 10 reports per user per day, enforced atomically (count +
88 // insert under a per-reporter advisory lock) so concurrent submits can't
89 // slip past the cap.
90 let created = db::reports::create_report_within_daily_limit(
91 &db,
92 user.id,
93 target_type,
94 target_id,
95 report_type,
96 reason,
97 10,
98 )
99 .await?;
100 if created.is_none() {
101 return Err(AppError::validation(
102 "Report limit reached. Please try again later.".to_string(),
103 ));
104 }
105
106 tracing::info!(
107 reporter = %user.id,
108 target_type = %target_type,
109 target_id = %target_id,
110 report_type = %report_type,
111 "report submitted"
112 );
113
114 Ok(AlertTemplate::new(
115 "success",
116 "Report submitted. Thank you.",
117 ))
118 }
119