Skip to main content

max / makenotwork

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