first commit

This commit is contained in:
2026-03-14 20:50:05 -04:00
commit 0c9af957fe
25 changed files with 1122 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
use axum::{
extract::{Query, State},
Json,
};
use serde::{Deserialize, Serialize};
use shared::{errors::AppError, models::ReportType};
use sqlx::PgPool;
use uuid::Uuid;
#[derive(Deserialize)]
pub struct FeedQuery {
#[serde(default = "default_page")]
pub page: i64,
#[serde(default = "default_per_page")]
pub per_page: i64,
pub report_type: Option<ReportType>,
pub hours: Option<i32>,
}
fn default_page() -> i64 { 1 }
fn default_per_page() -> i64 { 20 }
#[derive(Serialize)]
pub struct FeedItem {
pub 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<chrono::Utc>,
}
pub async fn get_feed(
State(db): State<PgPool>,
Query(q): Query<FeedQuery>,
) -> Result<Json<Vec<FeedItem>>, AppError> {
let hours = q.hours.unwrap_or(24);
let offset = (q.page - 1) * q.per_page;
let rows = sqlx::query!(
r#"SELECT r.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.created_at > NOW() - ($1 || ' hours')::interval
ORDER BY r.created_at DESC
LIMIT $2 OFFSET $3"#,
hours.to_string(),
q.per_page,
offset
)
.fetch_all(&db)
.await?;
let items = rows.into_iter().map(|r| FeedItem {
id: r.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(items))
}

View File

@@ -0,0 +1,4 @@
pub mod feed;
pub mod poi;
pub mod reports;
pub mod service_requests;

View File

@@ -0,0 +1,21 @@
use axum::Json;
use serde::Serialize;
use shared::errors::AppError;
#[derive(Serialize)]
pub struct PoiItem {
pub id: String,
pub name: String,
pub lat: f64,
pub lng: f64,
pub kind: String,
}
pub async fn get_fuel_poi() -> Result<Json<Vec<PoiItem>>, AppError> {
// Stub: return empty for now, populate from OSM data later
Ok(Json(vec![]))
}
pub async fn get_repair_poi() -> Result<Json<Vec<PoiItem>>, AppError> {
Ok(Json(vec![]))
}

View File

@@ -0,0 +1,138 @@
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<chrono::Utc>,
}
pub async fn create_report(
State(db): State<PgPool>,
Extension(claims): Extension<Claims>,
Json(req): Json<CreateReportRequest>,
) -> Result<Json<ReportResponse>, 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<PgPool>,
Query(q): Query<NearQuery>,
) -> Result<Json<Vec<ReportResponse>>, 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<PgPool>,
Extension(_claims): Extension<Claims>,
axum::extract::Path(id): axum::extract::Path<Uuid>,
) -> Result<Json<serde_json::Value>, 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<PgPool>,
Extension(_claims): Extension<Claims>,
axum::extract::Path(id): axum::extract::Path<Uuid>,
) -> Result<Json<serde_json::Value>, AppError> {
sqlx::query!("UPDATE reports SET flags = flags + 1 WHERE id = $1", id)
.execute(&db)
.await?;
Ok(Json(serde_json::json!({ "ok": true })))
}

View File

@@ -0,0 +1,116 @@
use axum::{
extract::{Extension, Path, State},
Json,
};
use serde::{Deserialize, Serialize};
use shared::{errors::AppError, models::{Claims, ServiceStatus, UserRole}};
use sqlx::PgPool;
use uuid::Uuid;
use validator::Validate;
#[derive(Deserialize, Validate)]
pub struct CreateServiceRequest {
#[validate(length(min = 10, max = 1000))]
pub description: String,
pub truck_details: serde_json::Value,
}
#[derive(Serialize)]
pub struct ServiceRequestResponse {
pub id: Uuid,
pub user_id: Uuid,
pub description: String,
pub truck_details: serde_json::Value,
pub status: ServiceStatus,
pub claimed_by: Option<Uuid>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
pub async fn create_service_request(
State(db): State<PgPool>,
Extension(claims): Extension<Claims>,
Json(req): Json<CreateServiceRequest>,
) -> Result<Json<ServiceRequestResponse>, AppError> {
req.validate()
.map_err(|e| AppError::BadRequest(e.to_string()))?;
if claims.role != UserRole::Driver {
return Err(AppError::Unauthorized("Only drivers can create service requests".into()));
}
let id = Uuid::new_v4();
let user_id: Uuid = claims.sub.parse().map_err(|_| AppError::Internal("bad uid".into()))?;
let now = chrono::Utc::now();
sqlx::query!(
"INSERT INTO service_requests (id, user_id, description, truck_details) VALUES ($1, $2, $3, $4)",
id, user_id, req.description, req.truck_details
)
.execute(&db)
.await?;
Ok(Json(ServiceRequestResponse {
id,
user_id,
description: req.description,
truck_details: req.truck_details,
status: ServiceStatus::Open,
claimed_by: None,
created_at: now,
}))
}
pub async fn get_open_service_requests(
State(db): State<PgPool>,
Extension(_claims): Extension<Claims>,
) -> Result<Json<Vec<ServiceRequestResponse>>, AppError> {
let rows = sqlx::query!(
r#"SELECT id, user_id, description, truck_details,
status as "status: ServiceStatus", claimed_by, created_at
FROM service_requests
WHERE status = 'open'::service_status
ORDER BY created_at DESC
LIMIT 50"#
)
.fetch_all(&db)
.await?;
let items = rows.into_iter().map(|r| ServiceRequestResponse {
id: r.id,
user_id: r.user_id,
description: r.description,
truck_details: r.truck_details,
status: r.status,
claimed_by: r.claimed_by,
created_at: r.created_at,
}).collect();
Ok(Json(items))
}
pub async fn claim_service_request(
State(db): State<PgPool>,
Extension(claims): Extension<Claims>,
Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, AppError> {
if claims.role != UserRole::MechanicShop && claims.role != UserRole::MobileMechanic {
return Err(AppError::Unauthorized("Only mechanics can claim service requests".into()));
}
let mechanic_id: Uuid = claims.sub.parse().map_err(|_| AppError::Internal("bad uid".into()))?;
let result = sqlx::query!(
r#"UPDATE service_requests
SET status = 'claimed'::service_status, claimed_by = $1
WHERE id = $2 AND status = 'open'::service_status"#,
mechanic_id, id
)
.execute(&db)
.await?;
if result.rows_affected() == 0 {
return Err(AppError::NotFound("Service request not found or already claimed".into()));
}
Ok(Json(serde_json::json!({ "ok": true })))
}