//! User-facing report submission endpoint. use axum::{Form, extract::State, response::IntoResponse}; use serde::Deserialize; use uuid::Uuid; use sqlx::PgPool; use crate::{ auth::AuthUser, db::{self, ReportTargetType, ReportType}, error::{AppError, Result}, templates::AlertTemplate, }; /// Form input for submitting a report. #[derive(Debug, Deserialize)] pub(super) struct ReportForm { pub target_type: String, pub target_id: String, pub report_type: String, #[serde(default)] pub reason: String, } /// Submit a report (logged-in users only). #[tracing::instrument(skip_all, name = "api::submit_report")] pub(super) async fn submit_report( State(db): State, AuthUser(user): AuthUser, Form(form): Form, ) -> Result { user.check_not_sandbox()?; let target_type: ReportTargetType = form .target_type .parse() .map_err(|_| AppError::validation("Invalid target type".to_string()))?; let target_id: Uuid = form .target_id .parse() .map_err(|_| AppError::validation("Invalid target ID".to_string()))?; let report_type: ReportType = form .report_type .parse() .map_err(|_| AppError::validation("Invalid report type".to_string()))?; // Require reason for "other" type let reason = form.reason.trim(); if report_type == ReportType::Other && reason.is_empty() { return Err(AppError::validation( "Please provide details for 'Other' reports".to_string(), )); } // Prevent self-reporting match target_type { ReportTargetType::Project => { let project = db::projects::get_project_by_id(&db, target_id.into()) .await? .ok_or(AppError::NotFound)?; if project.user_id == user.id { return Err(AppError::validation( "You cannot report your own project".to_string(), )); } } ReportTargetType::Item => { let item = db::items::get_item_by_id(&db, target_id.into()) .await? .ok_or(AppError::NotFound)?; let project = db::projects::get_project_by_id(&db, item.project_id) .await? .ok_or(AppError::NotFound)?; if project.user_id == user.id { return Err(AppError::validation( "You cannot report your own item".to_string(), )); } } } // Rate limit: max 10 reports per user per day, enforced atomically (count + // insert under a per-reporter advisory lock) so concurrent submits can't // slip past the cap. let created = db::reports::create_report_within_daily_limit( &db, user.id, target_type, target_id, report_type, reason, 10, ) .await?; if created.is_none() { return Err(AppError::validation( "Report limit reached. Please try again later.".to_string(), )); } tracing::info!( reporter = %user.id, target_type = %target_type, target_id = %target_id, report_type = %report_type, "report submitted" ); Ok(AlertTemplate::new( "success", "Report submitted. Thank you.", )) }