use chrono; use rocket::http::{Cookie, CookieJar, Status}; use rocket::serde::{json::Json, Deserialize, Serialize}; use rocket_db_pools::Connection; use uuid::Uuid; use bcrypt; use crate::Db; #[derive(Debug, Serialize)] #[serde(crate = "rocket::serde")] pub struct User { pub id: Uuid, pub username: String, pub password_hash: String, pub email: Option, pub display_name: Option, pub created_at: chrono::DateTime, } impl User { pub fn new(username: String, password_hash: String, email: Option, display_name: Option) -> Self { User { id: Uuid::new_v4(), username, password_hash, email, display_name, created_at: chrono::Utc::now(), } } } #[derive(Debug, Deserialize)] #[serde(crate = "rocket::serde")] pub struct NewUser { pub username: String, pub password: String, pub email: Option, pub display_name: Option, } #[derive(Debug, Deserialize)] #[serde(crate = "rocket::serde")] pub struct LoginCredentials { pub username: String, pub password: String, } #[derive(Debug, Serialize)] #[serde(crate = "rocket::serde")] pub struct LoginResponse { pub user_id: Uuid, pub username: String, } #[post("/users", data = "")] pub async fn create_user( mut db: Connection, new_user: Json, ) -> Result, Status> { let new_user = new_user.into_inner(); // Hash the password - we'll use bcrypt let password_hash = bcrypt::hash(new_user.password.as_bytes(), bcrypt::DEFAULT_COST) .map_err(|_| Status::InternalServerError)?; let user = User::new( new_user.username, password_hash, new_user.email, new_user.display_name ); let query = sqlx::query( "INSERT INTO users (id, username, password_hash, email, display_name, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)" ) .bind(user.id.to_string()) .bind(user.username.as_str()) .bind(user.password_hash.as_str()) .bind(user.email.as_ref()) .bind(user.display_name.as_ref()) .bind(user.created_at.to_rfc3339()) .execute(&mut **db).await; match query { Ok(_) => Ok(Json(user)), 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("/users")] pub async fn get_users(mut db: Connection) -> Result>, Status> { let query = sqlx::query!( r#" SELECT id as "id: String", username, password_hash, email, display_name, created_at as "created_at: chrono::DateTime" FROM users "# ) .fetch_all(&mut **db) .await .map_err(|e| { eprintln!("Database error: {}", e); Status::InternalServerError })?; // Convert the strings to UUIDs let users = query .into_iter() .map(|row| User { id: Uuid::parse_str(&row.id).unwrap(), username: row.username, password_hash: row.password_hash, email: row.email, display_name: row.display_name, created_at: row.created_at, }) .collect::>(); Ok(Json(users)) } #[delete("/users/")] pub async fn delete_user(mut db: Connection, user_id: &str) -> Status { // Validate UUID format let uuid = match Uuid::parse_str(user_id) { Ok(uuid) => uuid, Err(_) => return Status::BadRequest, }; let query = sqlx::query("DELETE FROM users WHERE id = ?") .bind(uuid.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 } } } #[post("/login", data = "")] pub async fn login( mut db: Connection, credentials: Json, cookies: &CookieJar<'_>, ) -> Result, Status> { let creds = credentials.into_inner(); // Find user by username let user = sqlx::query!( r#" SELECT id as "id: String", username, password_hash FROM users WHERE username = ? "#, creds.username ) .fetch_optional(&mut **db) .await .map_err(|e| { eprintln!("Database error: {}", e); Status::InternalServerError })?; let user = match user { Some(user) => user, None => return Err(Status::Unauthorized), }; // Verify password let valid = bcrypt::verify(creds.password.as_bytes(), &user.password_hash) .map_err(|_| Status::InternalServerError)?; if !valid { return Err(Status::Unauthorized); } // Set session cookie let user_id = Uuid::parse_str(&user.id).map_err(|_| Status::InternalServerError)?; cookies.add_private(Cookie::new("user_id", user_id.to_string())); Ok(Json(LoginResponse { user_id, username: user.username, })) } #[post("/logout")] pub fn logout(cookies: &CookieJar<'_>) -> Status { cookies.remove_private(Cookie::named("user_id")); Status::NoContent } // Add auth guard pub struct AuthenticatedUser { pub user_id: Uuid, } #[rocket::async_trait] impl<'r> rocket::request::FromRequest<'r> for AuthenticatedUser { type Error = (); async fn from_request(request: &'r rocket::Request<'_>) -> rocket::request::Outcome { let cookies = request.cookies(); if let Some(user_id_cookie) = cookies.get_private("user_id") { if let Ok(user_id) = Uuid::parse_str(user_id_cookie.value()) { return rocket::request::Outcome::Success(AuthenticatedUser { user_id }); } } rocket::request::Outcome::Error((rocket::http::Status::Unauthorized, ())) } }