use axum::{ extract::{Extension, Query, State}, Json, }; use chrono::Utc; use serde::{Deserialize, Serialize}; use shared::{errors::AppError, models::{Claims, ReportType}}; use sqlx::PgPool; use uuid::Uuid; use validator::Validate; #[derive(Debug, Deserialize, Validate)] pub struct CreateReportRequest { pub lat: f64, pub lng: f64, pub report_type: ReportType, #[validate(length(min = 1, max = 500))] pub text: String, } #[derive(Debug, Deserialize)] pub struct NearQuery { pub min_lat: f64, pub min_lng: f64, pub max_lat: f64, pub max_lng: f64, #[serde(default = "default_limit")] pub limit: i64, } fn default_limit() -> i64 { 100 } #[derive(Debug, Serialize)] pub struct ReportResponse { pub id: Uuid, pub user_id: Uuid, pub handle: String, pub report_type: ReportType, pub lat: f64, pub lng: f64, pub text: String, pub upvotes: i32, pub flags: i32, pub created_at: chrono::DateTime, } pub async fn create_report( State(db): State, Extension(claims): Extension, Json(req): Json, ) -> Result, AppError> { req.validate() .map_err(|e| AppError::BadRequest(e.to_string()))?; let id = Uuid::new_v4(); let user_id: Uuid = claims.sub.parse().map_err(|_| AppError::Internal("bad uid".into()))?; let report_type = req.report_type.clone(); sqlx::query!( r#"INSERT INTO reports (id, user_id, report_type, geom, text) VALUES ($1, $2, $3::report_type, ST_SetSRID(ST_MakePoint($5, $4), 4326), $6)"#, id, user_id, report_type as ReportType, req.lat, req.lng, req.text ) .execute(&db) .await?; Ok(Json(ReportResponse { id, user_id, handle: claims.handle, report_type: req.report_type, lat: req.lat, lng: req.lng, text: req.text, upvotes: 0, flags: 0, created_at: Utc::now(), })) } pub async fn get_reports_near( State(db): State, Query(q): Query, ) -> Result>, AppError> { let rows = sqlx::query!( r#"SELECT r.id, r.user_id, u.handle, r.report_type as "report_type: ReportType", ST_Y(r.geom::geometry) as lat, ST_X(r.geom::geometry) as lng, r.text, r.upvotes, r.flags, r.created_at FROM reports r JOIN users u ON u.id = r.user_id WHERE r.geom && ST_MakeEnvelope($1, $2, $3, $4, 4326) AND r.created_at > NOW() - INTERVAL '24 hours' ORDER BY r.created_at DESC LIMIT $5"#, q.min_lng, q.min_lat, q.max_lng, q.max_lat, q.limit ) .fetch_all(&db) .await?; let reports = rows.into_iter().map(|r| ReportResponse { id: r.id, user_id: r.user_id, handle: r.handle, report_type: r.report_type, lat: r.lat.unwrap_or(0.0), lng: r.lng.unwrap_or(0.0), text: r.text, upvotes: r.upvotes, flags: r.flags, created_at: r.created_at, }).collect(); Ok(Json(reports)) } pub async fn upvote_report( State(db): State, Extension(_claims): Extension, axum::extract::Path(id): axum::extract::Path, ) -> Result, AppError> { sqlx::query!("UPDATE reports SET upvotes = upvotes + 1 WHERE id = $1", id) .execute(&db) .await?; Ok(Json(serde_json::json!({ "ok": true }))) } pub async fn flag_report( State(db): State, Extension(_claims): Extension, axum::extract::Path(id): axum::extract::Path, ) -> Result, AppError> { sqlx::query!("UPDATE reports SET flags = flags + 1 WHERE id = $1", id) .execute(&db) .await?; Ok(Json(serde_json::json!({ "ok": true }))) }