From e21b2b96623eadf360c3996078f03816622df7cc Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 16 Jun 2025 23:13:33 -0400 Subject: [PATCH] FMT --- backend/src/auth.rs | 25 +- backend/src/execution_monitor.rs | 135 +++++++--- backend/src/executors/mod.rs | 4 +- backend/src/main.rs | 4 +- backend/src/models/project.rs | 25 +- backend/src/models/task.rs | 64 +++-- backend/src/models/task_attempt_activity.rs | 30 ++- backend/src/models/user.rs | 23 +- backend/src/routes/filesystem.rs | 37 ++- backend/src/routes/health.rs | 2 +- backend/src/routes/mod.rs | 2 +- backend/src/routes/projects.rs | 100 +++++--- backend/src/routes/tasks.rs | 261 ++++++++++++++------ backend/src/routes/users.rs | 113 +++++---- 14 files changed, 550 insertions(+), 275 deletions(-) diff --git a/backend/src/auth.rs b/backend/src/auth.rs index 68a9711f..4687f5b8 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -2,7 +2,7 @@ use axum::{ async_trait, body::Body, extract::FromRequestParts, - http::{request::Parts, StatusCode, Request}, + http::{request::Parts, Request, StatusCode}, middleware::Next, response::Response, }; @@ -43,9 +43,13 @@ where } } -pub fn create_token(user_id: Uuid, email: String, is_admin: bool) -> Result { +pub fn create_token( + user_id: Uuid, + email: String, + is_admin: bool, +) -> Result { let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your-secret-key".to_string()); - + let expiration = chrono::Utc::now() .checked_add_signed(chrono::Duration::hours(24)) .expect("valid timestamp") @@ -79,7 +83,7 @@ pub async fn auth_middleware( next: Next, ) -> Result { let headers = request.headers(); - + let auth_header = headers .get("authorization") .and_then(|value| value.to_str().ok()) @@ -90,7 +94,7 @@ pub async fn auth_middleware( .ok_or(StatusCode::UNAUTHORIZED)?; let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your-secret-key".to_string()); - + let claims = decode::( token, &DecodingKey::from_secret(jwt_secret.as_ref()), @@ -106,13 +110,10 @@ pub async fn auth_middleware( .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; // Verify user exists in database - let user_exists = sqlx::query!( - "SELECT id FROM users WHERE id = $1", - claims.user_id - ) - .fetch_optional(pool) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let user_exists = sqlx::query!("SELECT id FROM users WHERE id = $1", claims.user_id) + .fetch_optional(pool) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; if user_exists.is_none() { return Err(StatusCode::UNAUTHORIZED); diff --git a/backend/src/execution_monitor.rs b/backend/src/execution_monitor.rs index 8245dbe3..a8e08bf3 100644 --- a/backend/src/execution_monitor.rs +++ b/backend/src/execution_monitor.rs @@ -6,8 +6,8 @@ use tokio::sync::Mutex; use uuid::Uuid; use crate::models::{ - task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity}, - task_attempt::{TaskAttempt, TaskAttemptStatus} + task_attempt::{TaskAttempt, TaskAttemptStatus}, + task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity}, }; #[derive(Debug)] @@ -25,24 +25,31 @@ pub struct AppState { pub async fn execution_monitor(app_state: AppState) { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); - + loop { interval.tick().await; - + // Check for orphaned task attempts with latest activity status = InProgress but no running execution - let inprogress_attempt_ids = match TaskAttemptActivity::find_attempts_with_latest_inprogress_status(&app_state.db_pool).await { - Ok(attempts) => attempts, - Err(e) => { - tracing::error!("Failed to query inprogress attempts: {}", e); - continue; - } - }; + let inprogress_attempt_ids = + match TaskAttemptActivity::find_attempts_with_latest_inprogress_status( + &app_state.db_pool, + ) + .await + { + Ok(attempts) => attempts, + Err(e) => { + tracing::error!("Failed to query inprogress attempts: {}", e); + continue; + } + }; for attempt_id in inprogress_attempt_ids { // Check if this attempt has a running execution let has_running_execution = { let executions = app_state.running_executions.lock().await; - executions.values().any(|exec| exec.task_attempt_id == attempt_id) + executions + .values() + .any(|exec| exec.task_attempt_id == attempt_id) }; if !has_running_execution { @@ -59,29 +66,39 @@ pub async fn execution_monitor(app_state: AppState) { &create_activity, activity_id, TaskAttemptStatus::Paused, - ).await { - tracing::error!("Failed to create paused activity for orphaned attempt: {}", e); + ) + .await + { + tracing::error!( + "Failed to create paused activity for orphaned attempt: {}", + e + ); } else { tracing::info!("Marked orphaned task attempt {} as paused", attempt_id); } } } - + // Check for task attempts with latest activity status = Init - let init_attempt_ids = match TaskAttemptActivity::find_attempts_with_latest_init_status(&app_state.db_pool).await { - Ok(attempts) => attempts, - Err(e) => { - tracing::error!("Failed to query init attempts: {}", e); - continue; - } - }; + let init_attempt_ids = + match TaskAttemptActivity::find_attempts_with_latest_init_status(&app_state.db_pool) + .await + { + Ok(attempts) => attempts, + Err(e) => { + tracing::error!("Failed to query init attempts: {}", e); + continue; + } + }; for attempt_id in init_attempt_ids { - // Check if we already have a running execution for this attempt { let executions = app_state.running_executions.lock().await; - if executions.values().any(|exec| exec.task_attempt_id == attempt_id) { + if executions + .values() + .any(|exec| exec.task_attempt_id == attempt_id) + { continue; } } @@ -101,10 +118,22 @@ pub async fn execution_monitor(app_state: AppState) { // Get the executor and start streaming execution let executor = task_attempt.get_executor(); - let child = match executor.execute_streaming(&app_state.db_pool, task_attempt.task_id, attempt_id, &task_attempt.worktree_path).await { + let child = match executor + .execute_streaming( + &app_state.db_pool, + task_attempt.task_id, + attempt_id, + &task_attempt.worktree_path, + ) + .await + { Ok(child) => child, Err(e) => { - tracing::error!("Failed to start streaming execution for task attempt {}: {}", attempt_id, e); + tracing::error!( + "Failed to start streaming execution for task attempt {}: {}", + attempt_id, + e + ); continue; } }; @@ -113,11 +142,14 @@ pub async fn execution_monitor(app_state: AppState) { let execution_id = Uuid::new_v4(); { let mut executions = app_state.running_executions.lock().await; - executions.insert(execution_id, RunningExecution { - task_attempt_id: attempt_id, - child, - started_at: Utc::now(), - }); + executions.insert( + execution_id, + RunningExecution { + task_attempt_id: attempt_id, + child, + started_at: Utc::now(), + }, + ); } // Update task attempt activity to InProgress @@ -133,11 +165,17 @@ pub async fn execution_monitor(app_state: AppState) { &create_activity, activity_id, TaskAttemptStatus::InProgress, - ).await { + ) + .await + { tracing::error!("Failed to create in-progress activity: {}", e); } - tracing::info!("Started execution {} for task attempt {}", execution_id, attempt_id); + tracing::info!( + "Started execution {} for task attempt {}", + execution_id, + attempt_id + ); } // Check for completed processes @@ -149,14 +187,24 @@ pub async fn execution_monitor(app_state: AppState) { Ok(Some(status)) => { let success = status.success(); let exit_code = status.code(); - completed_executions.push((*execution_id, running_exec.task_attempt_id, success, exit_code)); + completed_executions.push(( + *execution_id, + running_exec.task_attempt_id, + success, + exit_code, + )); } Ok(None) => { // Still running } Err(e) => { tracing::error!("Error checking process status: {}", e); - completed_executions.push((*execution_id, running_exec.task_attempt_id, false, None)); + completed_executions.push(( + *execution_id, + running_exec.task_attempt_id, + false, + None, + )); } } } @@ -169,13 +217,17 @@ pub async fn execution_monitor(app_state: AppState) { // Handle completed executions for (execution_id, task_attempt_id, success, exit_code) in completed_executions { - let status_text = if success { "completed successfully" } else { "failed" }; + let status_text = if success { + "completed successfully" + } else { + "failed" + }; let exit_text = if let Some(code) = exit_code { format!(" with exit code {}", code) } else { String::new() }; - + tracing::info!("Execution {} {}{}", execution_id, status_text, exit_text); // Create task attempt activity with Paused status @@ -191,10 +243,15 @@ pub async fn execution_monitor(app_state: AppState) { &create_activity, activity_id, TaskAttemptStatus::Paused, - ).await { + ) + .await + { tracing::error!("Failed to create paused activity: {}", e); } else { - tracing::info!("Task attempt {} set to paused after execution completion", task_attempt_id); + tracing::info!( + "Task attempt {} set to paused after execution completion", + task_attempt_id + ); } } } diff --git a/backend/src/executors/mod.rs b/backend/src/executors/mod.rs index dddcf8e1..1f4e0a2a 100644 --- a/backend/src/executors/mod.rs +++ b/backend/src/executors/mod.rs @@ -1,5 +1,5 @@ -pub mod echo; pub mod claude; +pub mod echo; -pub use echo::EchoExecutor; pub use claude::ClaudeExecutor; +pub use echo::EchoExecutor; diff --git a/backend/src/main.rs b/backend/src/main.rs index 897f06c2..9bb5076d 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -19,8 +19,8 @@ mod routes; use auth::{auth_middleware, hash_password}; use execution_monitor::{execution_monitor, AppState}; -use models::{ApiResponse, user::User}; -use routes::{health, projects, tasks, users, filesystem}; +use models::{user::User, ApiResponse}; +use routes::{filesystem, health, projects, tasks, users}; async fn echo_handler( Json(payload): Json, diff --git a/backend/src/models/project.rs b/backend/src/models/project.rs index f8833bc7..96f55357 100644 --- a/backend/src/models/project.rs +++ b/backend/src/models/project.rs @@ -52,7 +52,10 @@ impl Project { .await } - pub async fn find_by_git_repo_path(pool: &PgPool, git_repo_path: &str) -> Result, sqlx::Error> { + pub async fn find_by_git_repo_path( + pool: &PgPool, + git_repo_path: &str, + ) -> Result, sqlx::Error> { sqlx::query_as!( Project, "SELECT id, name, git_repo_path, owner_id, created_at, updated_at FROM projects WHERE git_repo_path = $1", @@ -62,7 +65,11 @@ impl Project { .await } - pub async fn find_by_git_repo_path_excluding_id(pool: &PgPool, git_repo_path: &str, exclude_id: Uuid) -> Result, sqlx::Error> { + pub async fn find_by_git_repo_path_excluding_id( + pool: &PgPool, + git_repo_path: &str, + exclude_id: Uuid, + ) -> Result, sqlx::Error> { sqlx::query_as!( Project, "SELECT id, name, git_repo_path, owner_id, created_at, updated_at FROM projects WHERE git_repo_path = $1 AND id != $2", @@ -73,7 +80,12 @@ impl Project { .await } - pub async fn create(pool: &PgPool, data: &CreateProject, owner_id: Uuid, project_id: Uuid) -> Result { + pub async fn create( + pool: &PgPool, + data: &CreateProject, + owner_id: Uuid, + project_id: Uuid, + ) -> Result { sqlx::query_as!( Project, "INSERT INTO projects (id, name, git_repo_path, owner_id) VALUES ($1, $2, $3, $4) RETURNING id, name, git_repo_path, owner_id, created_at, updated_at", @@ -86,7 +98,12 @@ impl Project { .await } - pub async fn update(pool: &PgPool, id: Uuid, name: String, git_repo_path: String) -> Result { + pub async fn update( + pool: &PgPool, + id: Uuid, + name: String, + git_repo_path: String, + ) -> Result { sqlx::query_as!( Project, "UPDATE projects SET name = $2, git_repo_path = $3 WHERE id = $1 RETURNING id, name, git_repo_path, owner_id, created_at, updated_at", diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs index 82eec894..af1316d3 100644 --- a/backend/src/models/task.rs +++ b/backend/src/models/task.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type, PgPool}; +use sqlx::{FromRow, PgPool, Type}; use ts_rs::TS; use uuid::Uuid; @@ -58,7 +58,10 @@ pub struct UpdateTask { } impl Task { - pub async fn find_by_project_id(pool: &PgPool, project_id: Uuid) -> Result, sqlx::Error> { + pub async fn find_by_project_id( + pool: &PgPool, + project_id: Uuid, + ) -> Result, sqlx::Error> { sqlx::query_as!( Task, r#"SELECT id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at @@ -71,7 +74,10 @@ impl Task { .await } - pub async fn find_by_project_id_with_attempt_status(pool: &PgPool, project_id: Uuid) -> Result, sqlx::Error> { + pub async fn find_by_project_id_with_attempt_status( + pool: &PgPool, + project_id: Uuid, + ) -> Result, sqlx::Error> { let records = sqlx::query!( r#"SELECT t.id, @@ -102,16 +108,19 @@ impl Task { .fetch_all(pool) .await?; - let tasks = records.into_iter().map(|record| TaskWithAttemptStatus { - id: record.id, - project_id: record.project_id, - title: record.title, - description: record.description, - status: record.status, - created_at: record.created_at, - updated_at: record.updated_at, - has_in_progress_attempt: record.has_in_progress_attempt, - }).collect(); + let tasks = records + .into_iter() + .map(|record| TaskWithAttemptStatus { + id: record.id, + project_id: record.project_id, + title: record.title, + description: record.description, + status: record.status, + created_at: record.created_at, + updated_at: record.updated_at, + has_in_progress_attempt: record.has_in_progress_attempt, + }) + .collect(); Ok(tasks) } @@ -128,7 +137,11 @@ impl Task { .await } - pub async fn find_by_id_and_project_id(pool: &PgPool, id: Uuid, project_id: Uuid) -> Result, sqlx::Error> { + pub async fn find_by_id_and_project_id( + pool: &PgPool, + id: Uuid, + project_id: Uuid, + ) -> Result, sqlx::Error> { sqlx::query_as!( Task, r#"SELECT id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at @@ -141,7 +154,11 @@ impl Task { .await } - pub async fn create(pool: &PgPool, data: &CreateTask, task_id: Uuid) -> Result { + pub async fn create( + pool: &PgPool, + data: &CreateTask, + task_id: Uuid, + ) -> Result { sqlx::query_as!( Task, r#"INSERT INTO tasks (id, project_id, title, description, status) @@ -157,7 +174,14 @@ impl Task { .await } - pub async fn update(pool: &PgPool, id: Uuid, project_id: Uuid, title: String, description: Option, status: TaskStatus) -> Result { + pub async fn update( + pool: &PgPool, + id: Uuid, + project_id: Uuid, + title: String, + description: Option, + status: TaskStatus, + ) -> Result { sqlx::query_as!( Task, r#"UPDATE tasks @@ -176,8 +200,8 @@ impl Task { pub async fn delete(pool: &PgPool, id: Uuid, project_id: Uuid) -> Result { let result = sqlx::query!( - "DELETE FROM tasks WHERE id = $1 AND project_id = $2", - id, + "DELETE FROM tasks WHERE id = $1 AND project_id = $2", + id, project_id ) .execute(pool) @@ -187,8 +211,8 @@ impl Task { pub async fn exists(pool: &PgPool, id: Uuid, project_id: Uuid) -> Result { let result = sqlx::query!( - "SELECT id FROM tasks WHERE id = $1 AND project_id = $2", - id, + "SELECT id FROM tasks WHERE id = $1 AND project_id = $2", + id, project_id ) .fetch_optional(pool) diff --git a/backend/src/models/task_attempt_activity.rs b/backend/src/models/task_attempt_activity.rs index fb963687..6016b116 100644 --- a/backend/src/models/task_attempt_activity.rs +++ b/backend/src/models/task_attempt_activity.rs @@ -25,7 +25,10 @@ pub struct CreateTaskAttemptActivity { } impl TaskAttemptActivity { - pub async fn find_by_attempt_id(pool: &PgPool, attempt_id: Uuid) -> Result, sqlx::Error> { + pub async fn find_by_attempt_id( + pool: &PgPool, + attempt_id: Uuid, + ) -> Result, sqlx::Error> { sqlx::query_as!( TaskAttemptActivity, r#"SELECT id, task_attempt_id, status as "status!: TaskAttemptStatus", note, created_at @@ -38,7 +41,12 @@ impl TaskAttemptActivity { .await } - pub async fn create(pool: &PgPool, data: &CreateTaskAttemptActivity, activity_id: Uuid, status: TaskAttemptStatus) -> Result { + pub async fn create( + pool: &PgPool, + data: &CreateTaskAttemptActivity, + activity_id: Uuid, + status: TaskAttemptStatus, + ) -> Result { sqlx::query_as!( TaskAttemptActivity, r#"INSERT INTO task_attempt_activities (id, task_attempt_id, status, note) @@ -53,7 +61,11 @@ impl TaskAttemptActivity { .await } - pub async fn create_initial(pool: &PgPool, attempt_id: Uuid, activity_id: Uuid) -> Result<(), sqlx::Error> { + pub async fn create_initial( + pool: &PgPool, + attempt_id: Uuid, + activity_id: Uuid, + ) -> Result<(), sqlx::Error> { sqlx::query!( r#"INSERT INTO task_attempt_activities (id, task_attempt_id, status, note) VALUES ($1, $2, $3, $4)"#, @@ -67,7 +79,9 @@ impl TaskAttemptActivity { Ok(()) } - pub async fn find_attempts_with_latest_init_status(pool: &PgPool) -> Result, sqlx::Error> { + pub async fn find_attempts_with_latest_init_status( + pool: &PgPool, + ) -> Result, sqlx::Error> { let records = sqlx::query!( r#"SELECT DISTINCT ta.id FROM task_attempts ta @@ -83,11 +97,13 @@ impl TaskAttemptActivity { ) .fetch_all(pool) .await?; - + Ok(records.into_iter().map(|r| r.id).collect()) } - pub async fn find_attempts_with_latest_inprogress_status(pool: &PgPool) -> Result, sqlx::Error> { + pub async fn find_attempts_with_latest_inprogress_status( + pool: &PgPool, + ) -> Result, sqlx::Error> { let records = sqlx::query!( r#"SELECT DISTINCT ta.id FROM task_attempts ta @@ -103,7 +119,7 @@ impl TaskAttemptActivity { ) .fetch_all(pool) .await?; - + Ok(records.into_iter().map(|r| r.id).collect()) } } diff --git a/backend/src/models/user.rs b/backend/src/models/user.rs index 1b26c803..4d680804 100644 --- a/backend/src/models/user.rs +++ b/backend/src/models/user.rs @@ -100,7 +100,12 @@ impl User { .await } - pub async fn create(pool: &PgPool, data: &CreateUser, password_hash: String, user_id: Uuid) -> Result { + pub async fn create( + pool: &PgPool, + data: &CreateUser, + password_hash: String, + user_id: Uuid, + ) -> Result { let is_admin = data.is_admin.unwrap_or(false); sqlx::query_as!( @@ -115,7 +120,13 @@ impl User { .await } - pub async fn update(pool: &PgPool, id: Uuid, email: String, password_hash: String, is_admin: bool) -> Result { + pub async fn update( + pool: &PgPool, + id: Uuid, + email: String, + password_hash: String, + is_admin: bool, + ) -> Result { sqlx::query_as!( User, "UPDATE users SET email = $2, password_hash = $3, is_admin = $4 WHERE id = $1 RETURNING id, email, password_hash, is_admin, created_at, updated_at", @@ -135,9 +146,13 @@ impl User { Ok(result.rows_affected()) } - pub async fn create_or_update_admin(pool: &PgPool, email: &str, password_hash: &str) -> Result<(), sqlx::Error> { + pub async fn create_or_update_admin( + pool: &PgPool, + email: &str, + password_hash: &str, + ) -> Result<(), sqlx::Error> { use chrono::Utc; - + // Check if admin already exists let existing_admin = sqlx::query!( "SELECT id, password_hash FROM users WHERE email = $1", diff --git a/backend/src/routes/filesystem.rs b/backend/src/routes/filesystem.rs index cdab2b05..5d0871a4 100644 --- a/backend/src/routes/filesystem.rs +++ b/backend/src/routes/filesystem.rs @@ -1,18 +1,17 @@ use axum::{ - routing::get, - Router, - Json, - response::Json as ResponseJson, - extract::{Query, Extension}, + extract::{Extension, Query}, http::StatusCode, + response::Json as ResponseJson, + routing::get, + Json, Router, }; use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; use std::fs; +use std::path::{Path, PathBuf}; use ts_rs::TS; -use crate::models::ApiResponse; use crate::auth::AuthUser; +use crate::models::ApiResponse; #[derive(Debug, Serialize, TS)] #[ts(export)] @@ -61,25 +60,25 @@ pub async fn list_directory( match fs::read_dir(path) { Ok(entries) => { let mut directory_entries = Vec::new(); - + for entry in entries { if let Ok(entry) = entry { let path = entry.path(); let metadata = entry.metadata().ok(); - + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { // Skip hidden files/directories if name.starts_with('.') && name != ".." { continue; } - + let is_directory = metadata.map_or(false, |m| m.is_dir()); let is_git_repo = if is_directory { path.join(".git").exists() } else { false }; - + directory_entries.push(DirectoryEntry { name: name.to_string(), path: path.to_string_lossy().to_string(), @@ -89,16 +88,14 @@ pub async fn list_directory( } } } - + // Sort: directories first, then files, both alphabetically - directory_entries.sort_by(|a, b| { - match (a.is_directory, b.is_directory) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), - } + directory_entries.sort_by(|a, b| match (a.is_directory, b.is_directory) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), }); - + Ok(ResponseJson(ApiResponse { success: true, data: Some(directory_entries), @@ -125,7 +122,7 @@ pub async fn validate_git_path( // Check if path exists and is a git repo let is_valid_git_repo = path.exists() && path.is_dir() && path.join(".git").exists(); - + Ok(ResponseJson(ApiResponse { success: true, data: Some(is_valid_git_repo), diff --git a/backend/src/routes/health.rs b/backend/src/routes/health.rs index 1df6d6a4..46c44275 100644 --- a/backend/src/routes/health.rs +++ b/backend/src/routes/health.rs @@ -1,5 +1,5 @@ -use axum::response::Json; use crate::models::ApiResponse; +use axum::response::Json; pub async fn health_check() -> Json> { Json(ApiResponse { diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 839971c2..863ef02e 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -1,5 +1,5 @@ +pub mod filesystem; pub mod health; pub mod projects; pub mod tasks; pub mod users; -pub mod filesystem; diff --git a/backend/src/routes/projects.rs b/backend/src/routes/projects.rs index 357c8c4f..952e71e5 100644 --- a/backend/src/routes/projects.rs +++ b/backend/src/routes/projects.rs @@ -1,20 +1,22 @@ use axum::{ - routing::get, - Router, - Json, - response::Json as ResponseJson, - extract::{Path, Extension}, + extract::{Extension, Path}, http::StatusCode, + response::Json as ResponseJson, + routing::get, + Json, Router, }; use sqlx::PgPool; use uuid::Uuid; -use crate::models::{ApiResponse, project::{Project, CreateProject, UpdateProject}}; use crate::auth::AuthUser; +use crate::models::{ + project::{CreateProject, Project, UpdateProject}, + ApiResponse, +}; pub async fn get_projects( _auth: AuthUser, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>>, StatusCode> { match Project::find_all(&pool).await { Ok(projects) => Ok(ResponseJson(ApiResponse { @@ -32,7 +34,7 @@ pub async fn get_projects( pub async fn get_project( _auth: AuthUser, Path(id): Path, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>, StatusCode> { match Project::find_by_id(&pool, id).await { Ok(Some(project)) => Ok(ResponseJson(ApiResponse { @@ -51,11 +53,15 @@ pub async fn get_project( pub async fn create_project( auth: AuthUser, Extension(pool): Extension, - Json(payload): Json + Json(payload): Json, ) -> Result>, StatusCode> { let id = Uuid::new_v4(); - tracing::debug!("Creating project '{}' for user {}", payload.name, auth.user_id); + tracing::debug!( + "Creating project '{}' for user {}", + payload.name, + auth.user_id + ); // Check if git repo path is already used by another project match Project::find_by_git_repo_path(&pool, &payload.git_repo_path).await { @@ -77,7 +83,7 @@ pub async fn create_project( // Validate and setup git repository let path = std::path::Path::new(&payload.git_repo_path); - + if payload.use_existing_repo { // For existing repos, validate that the path exists and is a git repository if !path.exists() { @@ -105,7 +111,7 @@ pub async fn create_project( } } else { // For new repos, create directory and initialize git - + // Create directory if it doesn't exist if !path.exists() { if let Err(e) = std::fs::create_dir_all(path) { @@ -164,7 +170,7 @@ pub async fn create_project( pub async fn update_project( Path(id): Path, Extension(pool): Extension, - Json(payload): Json + Json(payload): Json, ) -> Result>, StatusCode> { // Check if project exists first let existing_project = match Project::find_by_id(&pool, id).await { @@ -184,7 +190,9 @@ pub async fn update_project( return Ok(ResponseJson(ApiResponse { success: false, data: None, - message: Some("A project with this git repository path already exists".to_string()), + message: Some( + "A project with this git repository path already exists".to_string(), + ), })); } Ok(None) => { @@ -200,7 +208,9 @@ pub async fn update_project( // Use existing values if not provided in update let name = payload.name.unwrap_or(existing_project.name); - let git_repo_path = payload.git_repo_path.unwrap_or(existing_project.git_repo_path.clone()); + let git_repo_path = payload + .git_repo_path + .unwrap_or(existing_project.git_repo_path.clone()); match Project::update(&pool, id, name, git_repo_path).await { Ok(project) => Ok(ResponseJson(ApiResponse { @@ -217,7 +227,7 @@ pub async fn update_project( pub async fn delete_project( Path(id): Path, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>, StatusCode> { match Project::delete(&pool, id).await { Ok(rows_affected) => { @@ -241,18 +251,24 @@ pub async fn delete_project( pub fn projects_router() -> Router { Router::new() .route("/projects", get(get_projects).post(create_project)) - .route("/projects/:id", get(get_project).put(update_project).delete(delete_project)) + .route( + "/projects/:id", + get(get_project).put(update_project).delete(delete_project), + ) } #[cfg(test)] mod tests { use super::*; + use crate::auth::{hash_password, AuthUser}; + use crate::models::{ + project::{CreateProject, UpdateProject}, + user::User, + }; use axum::extract::Extension; + use chrono::Utc; use sqlx::PgPool; use uuid::Uuid; - use chrono::Utc; - use crate::models::{user::User, project::{CreateProject, UpdateProject}}; - use crate::auth::{AuthUser, hash_password}; async fn create_test_user(pool: &PgPool, email: &str, password: &str, is_admin: bool) -> User { let id = Uuid::new_v4(); @@ -274,7 +290,12 @@ mod tests { .unwrap() } - async fn create_test_project(pool: &PgPool, name: &str, git_repo_path: &str, owner_id: Uuid) -> Project { + async fn create_test_project( + pool: &PgPool, + name: &str, + git_repo_path: &str, + owner_id: Uuid, + ) -> Project { let id = Uuid::new_v4(); let now = Utc::now(); @@ -296,7 +317,7 @@ mod tests { #[sqlx::test] async fn test_get_projects_success(pool: PgPool) { let user = create_test_user(&pool, "test@example.com", "password123", false).await; - + // Create multiple projects create_test_project(&pool, "Project 1", "/tmp/test1", user.id).await; create_test_project(&pool, "Project 2", "/tmp/test2", user.id).await; @@ -310,7 +331,7 @@ mod tests { let result = get_projects(auth, Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -329,7 +350,7 @@ mod tests { let result = get_projects(auth, Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -349,7 +370,7 @@ mod tests { let result = get_project(auth, Path(project.id), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -393,7 +414,7 @@ mod tests { let result = create_project(auth.clone(), Extension(pool), Json(create_request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -421,7 +442,7 @@ mod tests { let result = create_project(auth.clone(), Extension(pool), Json(create_request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -442,7 +463,7 @@ mod tests { let result = update_project(Path(project.id), Extension(pool), Json(update_request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -465,7 +486,7 @@ mod tests { let result = update_project(Path(project.id), Extension(pool), Json(update_request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -483,7 +504,12 @@ mod tests { git_repo_path: None, }; - let result = update_project(Path(nonexistent_project_id), Extension(pool), Json(update_request)).await; + let result = update_project( + Path(nonexistent_project_id), + Extension(pool), + Json(update_request), + ) + .await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); } @@ -491,11 +517,12 @@ mod tests { #[sqlx::test] async fn test_delete_project_success(pool: PgPool) { let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Project to Delete", "/tmp/to-delete", user.id).await; + let project = + create_test_project(&pool, "Project to Delete", "/tmp/to-delete", user.id).await; let result = delete_project(Path(project.id), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert_eq!(response.message.unwrap(), "Project deleted successfully"); @@ -513,10 +540,11 @@ mod tests { #[sqlx::test] async fn test_delete_project_cascades_to_tasks(pool: PgPool) { use crate::models::task::{Task, TaskStatus}; - + let user = create_test_user(&pool, "test@example.com", "password123", false).await; - let project = create_test_project(&pool, "Project with Tasks", "/tmp/with-tasks", user.id).await; - + let project = + create_test_project(&pool, "Project with Tasks", "/tmp/with-tasks", user.id).await; + // Create a task in the project let task_id = Uuid::new_v4(); let now = Utc::now(); @@ -563,7 +591,7 @@ mod tests { async fn test_projects_belong_to_users(pool: PgPool) { let user1 = create_test_user(&pool, "user1@example.com", "password123", false).await; let user2 = create_test_user(&pool, "user2@example.com", "password123", false).await; - + let project1 = create_test_project(&pool, "User 1 Project", "/tmp/user1", user1.id).await; let project2 = create_test_project(&pool, "User 2 Project", "/tmp/user2", user2.id).await; diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 756b1764..3addd993 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -1,27 +1,26 @@ use axum::{ - routing::get, - Router, - Json, - response::Json as ResponseJson, - extract::{Path, Extension}, + extract::{Extension, Path}, http::StatusCode, + response::Json as ResponseJson, + routing::get, + Json, Router, }; use sqlx::PgPool; use uuid::Uuid; -use crate::models::{ - ApiResponse, - project::Project, - task::{Task, CreateTask, UpdateTask, TaskWithAttemptStatus}, - task_attempt::{TaskAttempt, CreateTaskAttempt, TaskAttemptStatus}, - task_attempt_activity::{TaskAttemptActivity, CreateTaskAttemptActivity} -}; use crate::auth::AuthUser; +use crate::models::{ + project::Project, + task::{CreateTask, Task, TaskWithAttemptStatus, UpdateTask}, + task_attempt::{CreateTaskAttempt, TaskAttempt, TaskAttemptStatus}, + task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity}, + ApiResponse, +}; pub async fn get_project_tasks( _auth: AuthUser, Path(project_id): Path, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>>, StatusCode> { match Task::find_by_project_id_with_attempt_status(&pool, project_id).await { Ok(tasks) => Ok(ResponseJson(ApiResponse { @@ -39,7 +38,7 @@ pub async fn get_project_tasks( pub async fn get_task( _auth: AuthUser, Path((project_id, task_id)): Path<(Uuid, Uuid)>, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>, StatusCode> { match Task::find_by_id_and_project_id(&pool, task_id, project_id).await { Ok(Some(task)) => Ok(ResponseJson(ApiResponse { @@ -49,7 +48,12 @@ pub async fn get_task( })), Ok(None) => Err(StatusCode::NOT_FOUND), Err(e) => { - tracing::error!("Failed to fetch task {} in project {}: {}", task_id, project_id, e); + tracing::error!( + "Failed to fetch task {} in project {}: {}", + task_id, + project_id, + e + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } @@ -59,13 +63,13 @@ pub async fn create_task( Path(project_id): Path, auth: AuthUser, Extension(pool): Extension, - Json(mut payload): Json + Json(mut payload): Json, ) -> Result>, StatusCode> { let id = Uuid::new_v4(); - + // Ensure the project_id in the payload matches the path parameter payload.project_id = project_id; - + // Verify project exists first match Project::exists(&pool, project_id).await { Ok(false) => return Err(StatusCode::NOT_FOUND), @@ -76,7 +80,12 @@ pub async fn create_task( Ok(true) => {} } - tracing::debug!("Creating task '{}' in project {} for user {}", payload.title, project_id, auth.user_id); + tracing::debug!( + "Creating task '{}' in project {} for user {}", + payload.title, + project_id, + auth.user_id + ); match Task::create(&pool, &payload, id).await { Ok(task) => Ok(ResponseJson(ApiResponse { @@ -94,7 +103,7 @@ pub async fn create_task( pub async fn update_task( Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension, - Json(payload): Json + Json(payload): Json, ) -> Result>, StatusCode> { // Check if task exists in the specified project let existing_task = match Task::find_by_id_and_project_id(&pool, task_id, project_id).await { @@ -126,7 +135,7 @@ pub async fn update_task( pub async fn delete_task( Path((project_id, task_id)): Path<(Uuid, Uuid)>, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>, StatusCode> { match Task::delete(&pool, task_id, project_id).await { Ok(rows_affected) => { @@ -151,7 +160,7 @@ pub async fn delete_task( pub async fn get_task_attempts( _auth: AuthUser, Path((project_id, task_id)): Path<(Uuid, Uuid)>, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>>, StatusCode> { // Verify task exists in project first match Task::exists(&pool, task_id, project_id).await { @@ -179,7 +188,7 @@ pub async fn get_task_attempts( pub async fn get_task_attempt_activities( _auth: AuthUser, Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>>, StatusCode> { // Verify task attempt exists and belongs to the correct task match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { @@ -198,7 +207,11 @@ pub async fn get_task_attempt_activities( message: None, })), Err(e) => { - tracing::error!("Failed to fetch task attempt activities for attempt {}: {}", attempt_id, e); + tracing::error!( + "Failed to fetch task attempt activities for attempt {}: {}", + attempt_id, + e + ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } @@ -208,7 +221,7 @@ pub async fn create_task_attempt( _auth: AuthUser, Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension, - Json(mut payload): Json + Json(mut payload): Json, ) -> Result>, StatusCode> { // Verify task exists in project first match Task::exists(&pool, task_id, project_id).await { @@ -221,7 +234,7 @@ pub async fn create_task_attempt( } let id = Uuid::new_v4(); - + // Ensure the task_id in the payload matches the path parameter payload.task_id = task_id; @@ -248,7 +261,7 @@ pub async fn create_task_attempt_activity( _auth: AuthUser, Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Extension(pool): Extension, - Json(mut payload): Json + Json(mut payload): Json, ) -> Result>, StatusCode> { // Verify task attempt exists and belongs to the correct task match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { @@ -261,10 +274,10 @@ pub async fn create_task_attempt_activity( } let id = Uuid::new_v4(); - + // Ensure the task_attempt_id in the payload matches the path parameter payload.task_attempt_id = attempt_id; - + // Default to Init status if not provided let status = payload.status.clone().unwrap_or(TaskAttemptStatus::Init); @@ -285,7 +298,7 @@ pub async fn stop_task_attempt( _auth: AuthUser, Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Extension(pool): Extension, - Extension(app_state): Extension + Extension(app_state): Extension, ) -> Result>, StatusCode> { // Verify task attempt exists and belongs to the correct task match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { @@ -302,7 +315,7 @@ pub async fn stop_task_attempt( { let mut executions = app_state.running_executions.lock().await; let mut execution_id_to_remove = None; - + // Find the execution for this attempt for (exec_id, execution) in executions.iter_mut() { if execution.task_attempt_id == attempt_id { @@ -321,7 +334,7 @@ pub async fn stop_task_attempt( } } } - + // Remove the stopped execution from the map if let Some(exec_id) = execution_id_to_remove { executions.remove(&exec_id); @@ -349,7 +362,9 @@ pub async fn stop_task_attempt( &create_activity, activity_id, TaskAttemptStatus::Paused, - ).await { + ) + .await + { tracing::error!("Failed to create stopped activity: {}", e); return Err(StatusCode::INTERNAL_SERVER_ERROR); } @@ -362,25 +377,44 @@ pub async fn stop_task_attempt( } pub fn tasks_router() -> Router { - use axum::routing::{post, put, delete}; - + use axum::routing::{delete, post, put}; + Router::new() - .route("/projects/:project_id/tasks", get(get_project_tasks).post(create_task)) - .route("/projects/:project_id/tasks/:task_id", get(get_task).put(update_task).delete(delete_task)) - .route("/projects/:project_id/tasks/:task_id/attempts", get(get_task_attempts).post(create_task_attempt)) - .route("/projects/:project_id/tasks/:task_id/attempts/:attempt_id/activities", get(get_task_attempt_activities).post(create_task_attempt_activity)) - .route("/projects/:project_id/tasks/:task_id/attempts/:attempt_id/stop", post(stop_task_attempt)) + .route( + "/projects/:project_id/tasks", + get(get_project_tasks).post(create_task), + ) + .route( + "/projects/:project_id/tasks/:task_id", + get(get_task).put(update_task).delete(delete_task), + ) + .route( + "/projects/:project_id/tasks/:task_id/attempts", + get(get_task_attempts).post(create_task_attempt), + ) + .route( + "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/activities", + get(get_task_attempt_activities).post(create_task_attempt_activity), + ) + .route( + "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/stop", + post(stop_task_attempt), + ) } #[cfg(test)] mod tests { use super::*; + use crate::auth::{hash_password, AuthUser}; + use crate::models::{ + project::Project, + task::{CreateTask, TaskStatus, UpdateTask}, + user::User, + }; use axum::extract::Extension; + use chrono::Utc; use sqlx::PgPool; use uuid::Uuid; - use chrono::Utc; - use crate::models::{user::User, project::Project, task::{CreateTask, UpdateTask, TaskStatus}}; - use crate::auth::{AuthUser, hash_password}; async fn create_test_user(pool: &PgPool, email: &str, password: &str, is_admin: bool) -> User { let id = Uuid::new_v4(); @@ -422,7 +456,13 @@ mod tests { .unwrap() } - async fn create_test_task(pool: &PgPool, project_id: Uuid, title: &str, description: Option, status: TaskStatus) -> Task { + async fn create_test_task( + pool: &PgPool, + project_id: Uuid, + title: &str, + description: Option, + status: TaskStatus, + ) -> Task { let id = Uuid::new_v4(); let now = Utc::now(); @@ -446,11 +486,25 @@ mod tests { async fn test_get_project_tasks_success(pool: PgPool) { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let project = create_test_project(&pool, "Test Project", user.id).await; - + // Create multiple tasks - create_test_task(&pool, project.id, "Task 1", Some("Description 1".to_string()), TaskStatus::Todo).await; + create_test_task( + &pool, + project.id, + "Task 1", + Some("Description 1".to_string()), + TaskStatus::Todo, + ) + .await; create_test_task(&pool, project.id, "Task 2", None, TaskStatus::InProgress).await; - create_test_task(&pool, project.id, "Task 3", Some("Description 3".to_string()), TaskStatus::Done).await; + create_test_task( + &pool, + project.id, + "Task 3", + Some("Description 3".to_string()), + TaskStatus::Done, + ) + .await; let auth = AuthUser { user_id: user.id, @@ -460,7 +514,7 @@ mod tests { let result = get_project_tasks(auth, Path(project.id), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -480,7 +534,7 @@ mod tests { let result = get_project_tasks(auth, Path(project.id), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -491,7 +545,14 @@ mod tests { async fn test_get_task_success(pool: PgPool) { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let project = create_test_project(&pool, "Test Project", user.id).await; - let task = create_test_task(&pool, project.id, "Test Task", Some("Test Description".to_string()), TaskStatus::Todo).await; + let task = create_test_task( + &pool, + project.id, + "Test Task", + Some("Test Description".to_string()), + TaskStatus::Todo, + ) + .await; let auth = AuthUser { user_id: user.id, @@ -501,7 +562,7 @@ mod tests { let result = get_task(auth, Path((project.id, task.id)), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -524,7 +585,12 @@ mod tests { is_admin: false, }; - let result = get_task(auth, Path((project.id, nonexistent_task_id)), Extension(pool)).await; + let result = get_task( + auth, + Path((project.id, nonexistent_task_id)), + Extension(pool), + ) + .await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); } @@ -565,15 +631,24 @@ mod tests { description: Some("Task description".to_string()), }; - let result = create_task(Path(project.id), auth, Extension(pool), Json(create_request)).await; + let result = create_task( + Path(project.id), + auth, + Extension(pool), + Json(create_request), + ) + .await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); let created_task = response.data.unwrap(); assert_eq!(created_task.title, "New Task"); - assert_eq!(created_task.description, Some("Task description".to_string())); + assert_eq!( + created_task.description, + Some("Task description".to_string()) + ); assert_eq!(created_task.status, TaskStatus::Todo); assert_eq!(created_task.project_id, project.id); } @@ -595,7 +670,13 @@ mod tests { description: None, }; - let result = create_task(Path(nonexistent_project_id), auth, Extension(pool), Json(create_request)).await; + let result = create_task( + Path(nonexistent_project_id), + auth, + Extension(pool), + Json(create_request), + ) + .await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); } @@ -604,7 +685,14 @@ mod tests { async fn test_update_task_success(pool: PgPool) { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let project = create_test_project(&pool, "Test Project", user.id).await; - let task = create_test_task(&pool, project.id, "Original Title", Some("Original Description".to_string()), TaskStatus::Todo).await; + let task = create_test_task( + &pool, + project.id, + "Original Title", + Some("Original Description".to_string()), + TaskStatus::Todo, + ) + .await; let update_request = UpdateTask { title: Some("Updated Title".to_string()), @@ -612,15 +700,23 @@ mod tests { status: Some(TaskStatus::InProgress), }; - let result = update_task(Path((project.id, task.id)), Extension(pool), Json(update_request)).await; + let result = update_task( + Path((project.id, task.id)), + Extension(pool), + Json(update_request), + ) + .await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); let updated_task = response.data.unwrap(); assert_eq!(updated_task.title, "Updated Title"); - assert_eq!(updated_task.description, Some("Updated Description".to_string())); + assert_eq!( + updated_task.description, + Some("Updated Description".to_string()) + ); assert_eq!(updated_task.status, TaskStatus::InProgress); } @@ -628,7 +724,14 @@ mod tests { async fn test_update_task_partial(pool: PgPool) { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let project = create_test_project(&pool, "Test Project", user.id).await; - let task = create_test_task(&pool, project.id, "Original Title", Some("Original Description".to_string()), TaskStatus::Todo).await; + let task = create_test_task( + &pool, + project.id, + "Original Title", + Some("Original Description".to_string()), + TaskStatus::Todo, + ) + .await; // Only update status let update_request = UpdateTask { @@ -637,15 +740,23 @@ mod tests { status: Some(TaskStatus::Done), }; - let result = update_task(Path((project.id, task.id)), Extension(pool), Json(update_request)).await; + let result = update_task( + Path((project.id, task.id)), + Extension(pool), + Json(update_request), + ) + .await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); let updated_task = response.data.unwrap(); assert_eq!(updated_task.title, "Original Title"); // Should remain unchanged - assert_eq!(updated_task.description, Some("Original Description".to_string())); // Should remain unchanged + assert_eq!( + updated_task.description, + Some("Original Description".to_string()) + ); // Should remain unchanged assert_eq!(updated_task.status, TaskStatus::Done); // Should be updated } @@ -661,7 +772,12 @@ mod tests { status: None, }; - let result = update_task(Path((project.id, nonexistent_task_id)), Extension(pool), Json(update_request)).await; + let result = update_task( + Path((project.id, nonexistent_task_id)), + Extension(pool), + Json(update_request), + ) + .await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); } @@ -680,7 +796,12 @@ mod tests { }; // Try to update task in wrong project - let result = update_task(Path((project2.id, task.id)), Extension(pool), Json(update_request)).await; + let result = update_task( + Path((project2.id, task.id)), + Extension(pool), + Json(update_request), + ) + .await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); } @@ -689,11 +810,12 @@ mod tests { async fn test_delete_task_success(pool: PgPool) { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let project = create_test_project(&pool, "Test Project", user.id).await; - let task = create_test_task(&pool, project.id, "Task to Delete", None, TaskStatus::Todo).await; + let task = + create_test_task(&pool, project.id, "Task to Delete", None, TaskStatus::Todo).await; let result = delete_task(Path((project.id, task.id)), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert_eq!(response.message.unwrap(), "Task deleted successfully"); @@ -715,7 +837,8 @@ mod tests { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let project1 = create_test_project(&pool, "Project 1", user.id).await; let project2 = create_test_project(&pool, "Project 2", user.id).await; - let task = create_test_task(&pool, project1.id, "Task to Delete", None, TaskStatus::Todo).await; + let task = + create_test_task(&pool, project1.id, "Task to Delete", None, TaskStatus::Todo).await; // Try to delete task from wrong project let result = delete_task(Path((project2.id, task.id)), Extension(pool)).await; diff --git a/backend/src/routes/users.rs b/backend/src/routes/users.rs index 25602aa6..6afc5319 100644 --- a/backend/src/routes/users.rs +++ b/backend/src/routes/users.rs @@ -1,49 +1,45 @@ use axum::{ - routing::{get, post}, - Router, - Json, - response::Json as ResponseJson, - extract::{Path, Extension}, + extract::{Extension, Path}, http::StatusCode, + response::Json as ResponseJson, + routing::{get, post}, + Json, Router, }; use sqlx::PgPool; use uuid::Uuid; -use crate::models::{ApiResponse, user::{User, CreateUser, UpdateUser, LoginRequest, LoginResponse, UserResponse}}; -use crate::auth::{AuthUser, create_token, hash_password, verify_password}; +use crate::auth::{create_token, hash_password, verify_password, AuthUser}; +use crate::models::{ + user::{CreateUser, LoginRequest, LoginResponse, UpdateUser, User, UserResponse}, + ApiResponse, +}; pub async fn login( Extension(pool): Extension, - Json(payload): Json + Json(payload): Json, ) -> Result>, StatusCode> { match User::find_by_email(&pool, &payload.email).await { - Ok(Some(user)) => { - match verify_password(&payload.password, &user.password_hash) { - Ok(true) => { - match create_token(user.id, user.email.clone(), user.is_admin) { - Ok(token) => { - Ok(ResponseJson(ApiResponse { - success: true, - data: Some(LoginResponse { - user: user.into(), - token, - }), - message: Some("Login successful".to_string()), - })) - } - Err(e) => { - tracing::error!("Failed to create token: {}", e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } - Ok(false) => Err(StatusCode::UNAUTHORIZED), + Ok(Some(user)) => match verify_password(&payload.password, &user.password_hash) { + Ok(true) => match create_token(user.id, user.email.clone(), user.is_admin) { + Ok(token) => Ok(ResponseJson(ApiResponse { + success: true, + data: Some(LoginResponse { + user: user.into(), + token, + }), + message: Some("Login successful".to_string()), + })), Err(e) => { - tracing::error!("Password verification error: {}", e); + tracing::error!("Failed to create token: {}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } + }, + Ok(false) => Err(StatusCode::UNAUTHORIZED), + Err(e) => { + tracing::error!("Password verification error: {}", e); + Err(StatusCode::INTERNAL_SERVER_ERROR) } - } + }, Ok(None) => Err(StatusCode::UNAUTHORIZED), Err(e) => { tracing::error!("Failed to fetch user: {}", e); @@ -54,7 +50,7 @@ pub async fn login( pub async fn get_users( _auth: AuthUser, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>>, StatusCode> { match User::find_all(&pool).await { Ok(users) => { @@ -75,7 +71,7 @@ pub async fn get_users( pub async fn get_user( auth: AuthUser, Path(id): Path, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>, StatusCode> { // Users can only view their own profile unless they're admin if auth.user_id != id && !auth.is_admin { @@ -99,7 +95,7 @@ pub async fn get_user( pub async fn create_user( auth: AuthUser, Extension(pool): Extension, - Json(payload): Json + Json(payload): Json, ) -> Result>, StatusCode> { // Only admins can create users if !auth.is_admin { @@ -134,7 +130,7 @@ pub async fn update_user( auth: AuthUser, Path(id): Path, Extension(pool): Extension, - Json(payload): Json + Json(payload): Json, ) -> Result>, StatusCode> { // Users can only update their own profile unless they're admin if auth.user_id != id && !auth.is_admin { @@ -183,7 +179,7 @@ pub async fn update_user( pub async fn delete_user( auth: AuthUser, Path(id): Path, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>, StatusCode> { // Only admins can delete users, and they can't delete themselves if !auth.is_admin || auth.user_id == id { @@ -211,7 +207,7 @@ pub async fn delete_user( pub async fn get_current_user( auth: AuthUser, - Extension(pool): Extension + Extension(pool): Extension, ) -> Result>, StatusCode> { match User::find_by_id(&pool, auth.user_id).await { Ok(Some(user)) => Ok(ResponseJson(ApiResponse { @@ -227,9 +223,7 @@ pub async fn get_current_user( } } -pub async fn check_auth_status( - auth: AuthUser, -) -> ResponseJson> { +pub async fn check_auth_status(auth: AuthUser) -> ResponseJson> { ResponseJson(ApiResponse { success: true, data: Some(serde_json::json!({ @@ -243,8 +237,7 @@ pub async fn check_auth_status( } pub fn public_users_router() -> Router { - Router::new() - .route("/auth/login", post(login)) + Router::new().route("/auth/login", post(login)) } pub fn protected_users_router() -> Router { @@ -252,18 +245,21 @@ pub fn protected_users_router() -> Router { .route("/auth/status", get(check_auth_status)) .route("/auth/me", get(get_current_user)) .route("/users", get(get_users).post(create_user)) - .route("/users/:id", get(get_user).put(update_user).delete(delete_user)) + .route( + "/users/:id", + get(get_user).put(update_user).delete(delete_user), + ) } #[cfg(test)] mod tests { use super::*; + use crate::auth::{hash_password, AuthUser}; + use crate::models::user::{CreateUser, LoginRequest, UpdateUser}; use axum::extract::Extension; + use chrono::Utc; use sqlx::PgPool; use uuid::Uuid; - use chrono::Utc; - use crate::models::user::{LoginRequest, CreateUser, UpdateUser}; - use crate::auth::{AuthUser, hash_password}; async fn create_test_user(pool: &PgPool, email: &str, password: &str, is_admin: bool) -> User { let id = Uuid::new_v4(); @@ -288,7 +284,7 @@ mod tests { #[sqlx::test] async fn test_login_success(pool: PgPool) { let user = create_test_user(&pool, "test@example.com", "password123", false).await; - + let login_request = LoginRequest { email: "test@example.com".to_string(), password: "password123".to_string(), @@ -296,7 +292,7 @@ mod tests { let result = login(Extension(pool), Json(login_request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -306,7 +302,7 @@ mod tests { #[sqlx::test] async fn test_login_invalid_password(pool: PgPool) { create_test_user(&pool, "test@example.com", "password123", false).await; - + let login_request = LoginRequest { email: "test@example.com".to_string(), password: "wrongpassword".to_string(), @@ -343,7 +339,7 @@ mod tests { let result = get_users(auth, Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -362,7 +358,7 @@ mod tests { let result = get_user(auth, Path(user.id), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -398,7 +394,7 @@ mod tests { let result = get_user(auth, Path(regular_user.id), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -423,7 +419,7 @@ mod tests { let result = create_user(auth, Extension(pool), Json(create_request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -491,7 +487,7 @@ mod tests { let result = update_user(auth, Path(user.id), Extension(pool), Json(update_request)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -523,7 +519,8 @@ mod tests { #[sqlx::test] async fn test_delete_user_as_admin(pool: PgPool) { let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; - let user_to_delete = create_test_user(&pool, "delete@example.com", "password123", false).await; + let user_to_delete = + create_test_user(&pool, "delete@example.com", "password123", false).await; let auth = AuthUser { user_id: admin_user.id, @@ -533,7 +530,7 @@ mod tests { let result = delete_user(auth, Path(user_to_delete.id), Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert_eq!(response.message.unwrap(), "User deleted successfully"); @@ -582,7 +579,7 @@ mod tests { let result = get_current_user(auth, Extension(pool)).await; assert!(result.is_ok()); - + let response = result.unwrap().0; assert!(response.success); assert!(response.data.is_some()); @@ -600,7 +597,7 @@ mod tests { let response = check_auth_status(auth.clone()).await.0; assert!(response.success); assert!(response.data.is_some()); - + let data = response.data.unwrap(); assert_eq!(data["authenticated"], true); assert_eq!(data["user_id"], auth.user_id.to_string());