Skip to main content

max / makenotwork

3.3 KB · 116 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 let target_type: ReportTargetType = form
35 .target_type
36 .parse()
37 .map_err(|_| AppError::validation("Invalid target type".to_string()))?;
38
39 let target_id: Uuid = form
40 .target_id
41 .parse()
42 .map_err(|_| AppError::validation("Invalid target ID".to_string()))?;
43
44 let report_type: ReportType = form
45 .report_type
46 .parse()
47 .map_err(|_| AppError::validation("Invalid report type".to_string()))?;
48
49 // Require reason for "other" type
50 let reason = form.reason.trim();
51 if report_type == ReportType::Other && reason.is_empty() {
52 return Err(AppError::validation(
53 "Please provide details for 'Other' reports".to_string(),
54 ));
55 }
56
57 // Prevent self-reporting
58 match target_type {
59 ReportTargetType::Project => {
60 let project = db::projects::get_project_by_id(&db, target_id.into())
61 .await?
62 .ok_or(AppError::NotFound)?;
63 if project.user_id == user.id {
64 return Err(AppError::validation(
65 "You cannot report your own project".to_string(),
66 ));
67 }
68 }
69 ReportTargetType::Item => {
70 let item = db::items::get_item_by_id(&db, target_id.into())
71 .await?
72 .ok_or(AppError::NotFound)?;
73 let project = db::projects::get_project_by_id(&db, item.project_id)
74 .await?
75 .ok_or(AppError::NotFound)?;
76 if project.user_id == user.id {
77 return Err(AppError::validation(
78 "You cannot report your own item".to_string(),
79 ));
80 }
81 }
82 }
83
84 // Rate limit: max 10 reports per user per day, enforced atomically (count +
85 // insert under a per-reporter advisory lock) so concurrent submits can't
86 // slip past the cap.
87 let created = db::reports::create_report_within_daily_limit(
88 &db,
89 user.id,
90 target_type,
91 target_id,
92 report_type,
93 reason,
94 10,
95 )
96 .await?;
97 if created.is_none() {
98 return Err(AppError::validation(
99 "Report limit reached. Please try again later.".to_string(),
100 ));
101 }
102
103 tracing::info!(
104 reporter = %user.id,
105 target_type = %target_type,
106 target_id = %target_id,
107 report_type = %report_type,
108 "report submitted"
109 );
110
111 Ok(AlertTemplate::new(
112 "success",
113 "Report submitted. Thank you.",
114 ))
115 }
116