use chrono; use rocket::http::Status; use rocket::serde::{json::Json, Deserialize, Serialize}; use rocket_db_pools::Connection; use uuid::Uuid; use url::Url; use crate::Db; use crate::user::AuthenticatedUser; #[derive(Debug, Serialize)] #[serde(crate = "rocket::serde")] pub struct Feed { feed_id: Uuid, name: String, url: Url, user_id: Uuid, added_time: chrono::DateTime, last_checked_time: chrono::DateTime, } impl Feed { pub fn new(name: String, url: Url, user_id: Uuid) -> Self { let now = chrono::Utc::now(); Feed { feed_id: Uuid::new_v4(), name, url, user_id, added_time: now, last_checked_time: now, } } } #[derive(Debug, Deserialize)] #[serde(crate = "rocket::serde")] pub struct NewFeed { pub name: String, pub url: Url, // Changed from String to Url } #[post("/feeds", data = "")] pub async fn create_feed( mut db: Connection, new_feed: Json, user: AuthenticatedUser, ) -> Result, Status> { let new_feed = new_feed.into_inner(); // URL validation only needs to check scheme now if !new_feed.url.scheme().eq("http") && !new_feed.url.scheme().eq("https") { return Err(Status::UnprocessableEntity); } let feed = Feed::new(new_feed.name, new_feed.url, user.user_id); let query = sqlx::query( "INSERT INTO feeds (feed_id, name, url, user_id, added_time, last_checked_time) VALUES (?1, ?2, ?3, ?4, ?5, ?6)" ) .bind(feed.feed_id.to_string()) .bind(&feed.name) .bind(feed.url.as_str()) .bind(feed.user_id.to_string()) .bind(feed.added_time.to_rfc3339()) .bind(feed.last_checked_time.to_rfc3339()) .execute(&mut **db) .await; match query { Ok(_) => Ok(Json(feed)), Err(e) => { eprintln!("Database error: {}", e); match e { sqlx::Error::Database(db_err) if db_err.is_unique_violation() => { Err(Status::Conflict) } _ => Err(Status::InternalServerError), } } } } #[get("/feeds")] pub async fn list_feeds( mut db: Connection, user: AuthenticatedUser, ) -> Result>, Status> { let user_id = user.user_id.to_string(); let query = sqlx::query!( r#" SELECT feed_id as "feed_id: String", name, url, user_id as "user_id: String", added_time as "added_time: chrono::DateTime", last_checked_time as "last_checked_time: chrono::DateTime" FROM feeds WHERE user_id = ? ORDER BY added_time DESC "#, user_id ) .fetch_all(&mut **db) .await .map_err(|e| { eprintln!("Database error: {}", e); Status::InternalServerError })?; let feeds = query .into_iter() .map(|row| { // Parse URL from string let url = Url::parse(&row.url).map_err(|_| Status::InternalServerError)?; Ok(Feed { feed_id: Uuid::parse_str(&row.feed_id).unwrap(), name: row.name, url, user_id: Uuid::parse_str(&row.user_id).unwrap(), added_time: row.added_time, last_checked_time: row.last_checked_time, }) }) .collect::, Status>>()?; Ok(Json(feeds)) } #[delete("/feeds/")] pub async fn delete_feed( mut db: Connection, feed_id: &str, user: AuthenticatedUser, ) -> Status { // Validate UUID format let feed_uuid = match Uuid::parse_str(feed_id) { Ok(uuid) => uuid, Err(_) => return Status::BadRequest, }; let query = sqlx::query( "DELETE FROM feeds WHERE feed_id = ? AND user_id = ?" ) .bind(feed_uuid.to_string()) .bind(user.user_id.to_string()) .execute(&mut **db) .await; match query { Ok(result) => { if result.rows_affected() > 0 { Status::NoContent } else { Status::NotFound } } Err(e) => { eprintln!("Database error: {}", e); Status::InternalServerError } } }