From a709951fdc9400af240cf2ec58396eee992ca26d Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Tue, 17 Jun 2025 14:17:31 -0400 Subject: [PATCH] Squashed commit of the following: commit 70cb0b9de2bdbb6b564a7e6fb3a926a104e1e17c Author: Louis Knight-Webb Date: Tue Jun 17 14:16:45 2025 -0400 Update API commit 36a5161b96b8f034daa91d08d648be77fbdcb30b Author: Louis Knight-Webb Date: Tue Jun 17 14:14:33 2025 -0400 Further auth removal commit cba24ffd462a3de178658f26231011ed4d28a78b Author: Louis Knight-Webb Date: Tue Jun 17 14:03:13 2025 -0400 Fully remove users commit cfb1aec9b984c3374e5cc0ffe182de2647caf85d Author: Louis Knight-Webb Date: Tue Jun 17 11:51:20 2025 -0400 Start removing users --- backend/migrations/010_remove_users_table.sql | 16 + backend/src/auth.rs | 10 +- backend/src/bin/generate_types.rs | 15 - backend/src/main.rs | 45 +- backend/src/models/mod.rs | 1 - backend/src/models/project.rs | 18 +- backend/src/models/user.rs | 196 ------ backend/src/routes/filesystem.rs | 4 - backend/src/routes/mod.rs | 1 - backend/src/routes/projects.rs | 17 +- backend/src/routes/tasks.rs | 83 +-- backend/src/routes/users.rs | 607 ------------------ frontend/src/App.tsx | 36 +- frontend/src/components/auth/login-form.tsx | 118 ---- frontend/src/components/layout/navbar.tsx | 43 +- .../components/projects/project-detail.tsx | 16 +- .../src/components/projects/project-form.tsx | 6 +- .../src/components/projects/project-list.tsx | 6 +- .../components/tasks/TaskDetailsDialog.tsx | 12 +- frontend/src/components/ui/folder-picker.tsx | 4 +- frontend/src/components/users/user-form.tsx | 185 ------ frontend/src/components/users/user-list.tsx | 188 ------ frontend/src/components/users/users-page.tsx | 5 - frontend/src/contexts/auth-context.tsx | 122 ---- frontend/src/lib/api.ts | 11 + frontend/src/lib/auth.ts | 58 -- frontend/src/pages/home.tsx | 71 +- frontend/src/pages/project-tasks.tsx | 14 +- frontend/src/pages/task-attempt-compare.tsx | 6 +- frontend/src/pages/task-details.tsx | 14 +- frontend/src/pages/users.tsx | 188 ------ frontend/vite.config.ts | 1 - shared/types.ts | 12 +- 33 files changed, 148 insertions(+), 1981 deletions(-) create mode 100644 backend/migrations/010_remove_users_table.sql delete mode 100644 backend/src/models/user.rs delete mode 100644 backend/src/routes/users.rs delete mode 100644 frontend/src/components/auth/login-form.tsx delete mode 100644 frontend/src/components/users/user-form.tsx delete mode 100644 frontend/src/components/users/user-list.tsx delete mode 100644 frontend/src/components/users/users-page.tsx delete mode 100644 frontend/src/contexts/auth-context.tsx create mode 100644 frontend/src/lib/api.ts delete mode 100644 frontend/src/lib/auth.ts delete mode 100644 frontend/src/pages/users.tsx diff --git a/backend/migrations/010_remove_users_table.sql b/backend/migrations/010_remove_users_table.sql new file mode 100644 index 00000000..307d25b0 --- /dev/null +++ b/backend/migrations/010_remove_users_table.sql @@ -0,0 +1,16 @@ +-- Remove users table and all references to it + +-- Drop the trigger on users table +DROP TRIGGER IF EXISTS update_users_updated_at ON users; + +-- Drop indexes related to users +DROP INDEX IF EXISTS idx_users_email; +DROP INDEX IF EXISTS idx_users_is_admin; + +-- Drop the foreign key constraint and column from projects table +ALTER TABLE projects DROP CONSTRAINT projects_owner_id_fkey; +DROP INDEX IF EXISTS idx_projects_owner_id; +ALTER TABLE projects DROP COLUMN owner_id; + +-- Drop the users table +DROP TABLE IF EXISTS users; diff --git a/backend/src/auth.rs b/backend/src/auth.rs index 4687f5b8..773da3df 100644 --- a/backend/src/auth.rs +++ b/backend/src/auth.rs @@ -109,15 +109,7 @@ pub async fn auth_middleware( .get::() .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)?; - - if user_exists.is_none() { - return Err(StatusCode::UNAUTHORIZED); - } + // Note: User table removed, skipping database verification // Add user info to request extensions for handlers to access request.extensions_mut().insert(AuthUser { diff --git a/backend/src/bin/generate_types.rs b/backend/src/bin/generate_types.rs index 7873ee3b..5a2acb78 100644 --- a/backend/src/bin/generate_types.rs +++ b/backend/src/bin/generate_types.rs @@ -71,16 +71,6 @@ export {} export {} -export {} - -export {} - -export {} - -export {} - -export {} - export {}"#, bloop_backend::models::ApiResponse::<()>::decl(), bloop_backend::executor::ExecutorConfig::decl(), @@ -98,11 +88,6 @@ export {}"#, bloop_backend::models::task_attempt::UpdateTaskAttempt::decl(), bloop_backend::models::task_attempt_activity::TaskAttemptActivity::decl(), bloop_backend::models::task_attempt_activity::CreateTaskAttemptActivity::decl(), - bloop_backend::models::user::CreateUser::decl(), - bloop_backend::models::user::LoginRequest::decl(), - bloop_backend::models::user::LoginResponse::decl(), - bloop_backend::models::user::UpdateUser::decl(), - bloop_backend::models::user::UserResponse::decl(), bloop_backend::routes::filesystem::DirectoryEntry::decl(), bloop_backend::models::task_attempt::DiffChunkType::decl(), bloop_backend::models::task_attempt::DiffChunk::decl(), diff --git a/backend/src/main.rs b/backend/src/main.rs index 4789c0e9..f81f7d6f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,7 +2,6 @@ use axum::{ body::Body, extract::Extension, http::{header, HeaderValue, StatusCode}, - middleware, response::{IntoResponse, Json as ResponseJson, Response}, routing::{get, post}, Json, Router, @@ -20,10 +19,9 @@ mod executors; mod models; mod routes; -use auth::{auth_middleware, hash_password}; use execution_monitor::{execution_monitor, AppState}; -use models::{user::User, ApiResponse}; -use routes::{filesystem, health, projects, tasks, users}; +use models::ApiResponse; +use routes::{filesystem, health, projects, tasks}; #[derive(RustEmbed)] #[folder = "../frontend/dist"] @@ -105,11 +103,6 @@ async fn main() -> anyhow::Result<()> { .connect(&database_url) .await?; - // Create default admin account if it doesn't exist - if let Err(e) = create_admin_account(&pool).await { - tracing::warn!("Failed to create admin account: {}", e); - } - // Create app state let app_state = AppState { running_executions: Arc::new(Mutex::new(HashMap::new())), @@ -125,21 +118,22 @@ async fn main() -> anyhow::Result<()> { // Public routes (no auth required) let public_routes = Router::new() .route("/api/health", get(health::health_check)) - .route("/api/echo", post(echo_handler)) - .merge(users::public_users_router()); + .route("/api/echo", post(echo_handler)); - // Protected routes (auth required) - let protected_routes = Router::new() - .merge(projects::projects_router()) - .merge(tasks::tasks_router()) - .merge(users::protected_users_router()) - .merge(filesystem::filesystem_router()) - .layer(Extension(pool.clone())) - .layer(middleware::from_fn(auth_middleware)); + // All routes (no auth required) + let app_routes = Router::new() + .nest( + "/api", + Router::new() + .merge(projects::projects_router()) + .merge(tasks::tasks_router()) + .merge(filesystem::filesystem_router()), + ) + .layer(Extension(pool.clone())); let app = Router::new() .merge(public_routes) - .merge(protected_routes) + .merge(app_routes) // Static file serving routes .route("/", get(index_handler)) .route("/*path", get(static_handler)) @@ -155,14 +149,3 @@ async fn main() -> anyhow::Result<()> { Ok(()) } - -async fn create_admin_account(pool: &sqlx::PgPool) -> anyhow::Result<()> { - let admin_email = "admin@example.com"; - let admin_password = env::var("ADMIN_PASSWORD").unwrap_or_else(|_| "admin123".to_string()); - - let password_hash = hash_password(&admin_password)?; - - User::create_or_update_admin(pool, admin_email, &password_hash).await?; - - Ok(()) -} diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs index 1eff7217..26e8abda 100644 --- a/backend/src/models/mod.rs +++ b/backend/src/models/mod.rs @@ -3,6 +3,5 @@ pub mod project; pub mod task; pub mod task_attempt; pub mod task_attempt_activity; -pub mod user; pub use api_response::ApiResponse; diff --git a/backend/src/models/project.rs b/backend/src/models/project.rs index 96f55357..e42e3fb7 100644 --- a/backend/src/models/project.rs +++ b/backend/src/models/project.rs @@ -10,7 +10,7 @@ pub struct Project { pub id: Uuid, pub name: String, pub git_repo_path: String, - pub owner_id: Uuid, // Foreign key to User + #[ts(type = "Date")] pub created_at: DateTime, #[ts(type = "Date")] @@ -36,7 +36,7 @@ impl Project { pub async fn find_all(pool: &PgPool) -> Result, sqlx::Error> { sqlx::query_as!( Project, - "SELECT id, name, git_repo_path, owner_id, created_at, updated_at FROM projects ORDER BY created_at DESC" + "SELECT id, name, git_repo_path, created_at, updated_at FROM projects ORDER BY created_at DESC" ) .fetch_all(pool) .await @@ -45,7 +45,7 @@ impl Project { pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { sqlx::query_as!( Project, - "SELECT id, name, git_repo_path, owner_id, created_at, updated_at FROM projects WHERE id = $1", + "SELECT id, name, git_repo_path, created_at, updated_at FROM projects WHERE id = $1", id ) .fetch_optional(pool) @@ -58,7 +58,7 @@ impl Project { ) -> 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", + "SELECT id, name, git_repo_path, created_at, updated_at FROM projects WHERE git_repo_path = $1", git_repo_path ) .fetch_optional(pool) @@ -72,7 +72,7 @@ impl Project { ) -> 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", + "SELECT id, name, git_repo_path, created_at, updated_at FROM projects WHERE git_repo_path = $1 AND id != $2", git_repo_path, exclude_id ) @@ -83,16 +83,14 @@ impl Project { 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", + "INSERT INTO projects (id, name, git_repo_path) VALUES ($1, $2, $3) RETURNING id, name, git_repo_path, created_at, updated_at", project_id, data.name, - data.git_repo_path, - owner_id + data.git_repo_path ) .fetch_one(pool) .await @@ -106,7 +104,7 @@ impl Project { ) -> 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", + "UPDATE projects SET name = $2, git_repo_path = $3 WHERE id = $1 RETURNING id, name, git_repo_path, created_at, updated_at", id, name, git_repo_path diff --git a/backend/src/models/user.rs b/backend/src/models/user.rs deleted file mode 100644 index 4d680804..00000000 --- a/backend/src/models/user.rs +++ /dev/null @@ -1,196 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, PgPool}; -use ts_rs::TS; -use uuid::Uuid; - -#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] -pub struct User { - pub id: Uuid, - pub email: String, - #[serde(skip_serializing)] - pub password_hash: String, // Hashed password - pub is_admin: bool, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -#[derive(Debug, Deserialize, TS)] -#[ts(export)] -pub struct CreateUser { - pub email: String, - pub password: String, - pub is_admin: Option, -} - -#[derive(Debug, Deserialize, TS)] -#[ts(export)] -pub struct UpdateUser { - pub email: Option, - pub password: Option, - pub is_admin: Option, -} - -#[derive(Debug, Deserialize, TS)] -#[ts(export)] -pub struct LoginRequest { - pub email: String, - pub password: String, -} - -#[derive(Debug, Serialize, TS)] -#[ts(export)] -pub struct LoginResponse { - pub user: UserResponse, - pub token: String, -} - -#[derive(Debug, Serialize, TS)] -#[ts(export)] -#[ts(rename = "User")] -pub struct UserResponse { - pub id: Uuid, - pub email: String, - pub is_admin: bool, - #[ts(type = "Date")] - pub created_at: DateTime, - #[ts(type = "Date")] - pub updated_at: DateTime, -} - -impl From for UserResponse { - fn from(user: User) -> Self { - Self { - id: user.id, - email: user.email, - is_admin: user.is_admin, - created_at: user.created_at, - updated_at: user.updated_at, - } - } -} - -impl User { - pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error> { - sqlx::query_as!( - User, - "SELECT id, email, password_hash, is_admin, created_at, updated_at FROM users WHERE email = $1", - email - ) - .fetch_optional(pool) - .await - } - - pub async fn find_all(pool: &PgPool) -> Result, sqlx::Error> { - sqlx::query_as!( - User, - "SELECT id, email, password_hash, is_admin, created_at, updated_at FROM users ORDER BY created_at DESC" - ) - .fetch_all(pool) - .await - } - - pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { - sqlx::query_as!( - User, - "SELECT id, email, password_hash, is_admin, created_at, updated_at FROM users WHERE id = $1", - id - ) - .fetch_optional(pool) - .await - } - - 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!( - User, - "INSERT INTO users (id, email, password_hash, is_admin) VALUES ($1, $2, $3, $4) RETURNING id, email, password_hash, is_admin, created_at, updated_at", - user_id, - data.email, - password_hash, - is_admin - ) - .fetch_one(pool) - .await - } - - 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", - id, - email, - password_hash, - is_admin - ) - .fetch_one(pool) - .await - } - - pub async fn delete(pool: &PgPool, id: Uuid) -> Result { - let result = sqlx::query!("DELETE FROM users WHERE id = $1", id) - .execute(pool) - .await?; - Ok(result.rows_affected()) - } - - 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", - email - ) - .fetch_optional(pool) - .await?; - - if let Some(admin) = existing_admin { - // Update existing admin password - let now = Utc::now(); - sqlx::query!( - "UPDATE users SET password_hash = $2, is_admin = $3, updated_at = $4 WHERE id = $1", - admin.id, - password_hash, - true, - now - ) - .execute(pool) - .await?; - - tracing::info!("Updated admin account"); - } else { - // Create new admin account - let id = Uuid::new_v4(); - sqlx::query!( - "INSERT INTO users (id, email, password_hash, is_admin) VALUES ($1, $2, $3, $4)", - id, - email, - password_hash, - true - ) - .execute(pool) - .await?; - - tracing::info!("Created admin account: {}", email); - } - - Ok(()) - } -} diff --git a/backend/src/routes/filesystem.rs b/backend/src/routes/filesystem.rs index 5d0871a4..0fecdf7b 100644 --- a/backend/src/routes/filesystem.rs +++ b/backend/src/routes/filesystem.rs @@ -10,7 +10,6 @@ use std::fs; use std::path::{Path, PathBuf}; use ts_rs::TS; -use crate::auth::AuthUser; use crate::models::ApiResponse; #[derive(Debug, Serialize, TS)] @@ -28,7 +27,6 @@ pub struct ListDirectoryQuery { } pub async fn list_directory( - _auth: AuthUser, Query(query): Query, ) -> Result>>, StatusCode> { let path_str = query.path.unwrap_or_else(|| { @@ -114,7 +112,6 @@ pub async fn list_directory( } pub async fn validate_git_path( - _auth: AuthUser, Query(query): Query, ) -> Result>, StatusCode> { let path_str = query.path.ok_or(StatusCode::BAD_REQUEST)?; @@ -135,7 +132,6 @@ pub async fn validate_git_path( } pub async fn create_git_repo( - _auth: AuthUser, Query(query): Query, ) -> Result>, StatusCode> { let path_str = query.path.ok_or(StatusCode::BAD_REQUEST)?; diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 863ef02e..f6120cf8 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -2,4 +2,3 @@ pub mod filesystem; pub mod health; pub mod projects; pub mod tasks; -pub mod users; diff --git a/backend/src/routes/projects.rs b/backend/src/routes/projects.rs index 952e71e5..36bd06ca 100644 --- a/backend/src/routes/projects.rs +++ b/backend/src/routes/projects.rs @@ -8,14 +8,12 @@ use axum::{ use sqlx::PgPool; use uuid::Uuid; -use crate::auth::AuthUser; use crate::models::{ project::{CreateProject, Project, UpdateProject}, ApiResponse, }; pub async fn get_projects( - _auth: AuthUser, Extension(pool): Extension, ) -> Result>>, StatusCode> { match Project::find_all(&pool).await { @@ -32,7 +30,6 @@ pub async fn get_projects( } pub async fn get_project( - _auth: AuthUser, Path(id): Path, Extension(pool): Extension, ) -> Result>, StatusCode> { @@ -51,17 +48,12 @@ pub async fn get_project( } pub async fn create_project( - auth: AuthUser, Extension(pool): Extension, 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 '{}'", payload.name); // Check if git repo path is already used by another project match Project::find_by_git_repo_path(&pool, &payload.git_repo_path).await { @@ -154,7 +146,7 @@ pub async fn create_project( } } - match Project::create(&pool, &payload, auth.user_id, id).await { + match Project::create(&pool, &payload, id).await { Ok(project) => Ok(ResponseJson(ApiResponse { success: true, data: Some(project), @@ -261,10 +253,7 @@ pub fn projects_router() -> Router { mod tests { use super::*; use crate::auth::{hash_password, AuthUser}; - use crate::models::{ - project::{CreateProject, UpdateProject}, - user::User, - }; + use crate::models::project::{CreateProject, UpdateProject}; use axum::extract::Extension; use chrono::Utc; use sqlx::PgPool; diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 823c3700..2e3730f7 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -8,7 +8,6 @@ use axum::{ use sqlx::PgPool; use uuid::Uuid; -use crate::auth::AuthUser; use crate::models::{ project::Project, task::{CreateTask, Task, TaskWithAttemptStatus, UpdateTask}, @@ -18,7 +17,6 @@ use crate::models::{ }; pub async fn get_project_tasks( - _auth: AuthUser, Path(project_id): Path, Extension(pool): Extension, ) -> Result>>, StatusCode> { @@ -36,7 +34,6 @@ pub async fn get_project_tasks( } pub async fn get_task( - _auth: AuthUser, Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension, ) -> Result>, StatusCode> { @@ -61,7 +58,6 @@ pub async fn get_task( pub async fn create_task( Path(project_id): Path, - auth: AuthUser, Extension(pool): Extension, Json(mut payload): Json, ) -> Result>, StatusCode> { @@ -81,10 +77,9 @@ pub async fn create_task( } tracing::debug!( - "Creating task '{}' in project {} for user {}", + "Creating task '{}' in project {}", payload.title, - project_id, - auth.user_id + project_id ); match Task::create(&pool, &payload, id).await { @@ -158,7 +153,6 @@ pub async fn delete_task( // Task Attempts endpoints pub async fn get_task_attempts( - _auth: AuthUser, Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension, ) -> Result>>, StatusCode> { @@ -186,7 +180,6 @@ 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, ) -> Result>>, StatusCode> { @@ -218,7 +211,6 @@ pub async fn get_task_attempt_activities( } pub async fn create_task_attempt( - _auth: AuthUser, Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension, Json(mut payload): Json, @@ -258,7 +250,6 @@ pub async fn create_task_attempt( } 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, @@ -295,7 +286,6 @@ pub async fn create_task_attempt_activity( } 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, @@ -377,7 +367,6 @@ pub async fn stop_task_attempt( } pub async fn get_task_attempt_diff( - _auth: AuthUser, Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Extension(pool): Extension, ) -> Result>, StatusCode> { @@ -459,7 +448,7 @@ pub fn tasks_router() -> Router { #[cfg(test)] mod tests { use super::*; - use crate::auth::{hash_password, AuthUser}; + use crate::auth::hash_password; use crate::models::{ project::Project, task::{CreateTask, TaskStatus, UpdateTask}, @@ -560,13 +549,7 @@ mod tests { ) .await; - let auth = AuthUser { - user_id: user.id, - email: user.email, - is_admin: false, - }; - - let result = get_project_tasks(auth, Path(project.id), Extension(pool)).await; + let result = get_project_tasks(Path(project.id), Extension(pool)).await; assert!(result.is_ok()); let response = result.unwrap().0; @@ -580,13 +563,7 @@ mod tests { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let project = create_test_project(&pool, "Empty Project", user.id).await; - let auth = AuthUser { - user_id: user.id, - email: user.email, - is_admin: false, - }; - - let result = get_project_tasks(auth, Path(project.id), Extension(pool)).await; + let result = get_project_tasks(Path(project.id), Extension(pool)).await; assert!(result.is_ok()); let response = result.unwrap().0; @@ -608,13 +585,7 @@ mod tests { ) .await; - let auth = AuthUser { - user_id: user.id, - email: user.email, - is_admin: false, - }; - - let result = get_task(auth, Path((project.id, task.id)), Extension(pool)).await; + let result = get_task(Path((project.id, task.id)), Extension(pool)).await; assert!(result.is_ok()); let response = result.unwrap().0; @@ -633,18 +604,7 @@ mod tests { let project = create_test_project(&pool, "Test Project", user.id).await; let nonexistent_task_id = Uuid::new_v4(); - let auth = AuthUser { - user_id: user.id, - email: user.email, - is_admin: false, - }; - - let result = get_task( - auth, - Path((project.id, nonexistent_task_id)), - Extension(pool), - ) - .await; + let result = get_task(Path((project.id, nonexistent_task_id)), Extension(pool)).await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); } @@ -656,14 +616,8 @@ mod tests { let project2 = create_test_project(&pool, "Project 2", user.id).await; let task = create_test_task(&pool, project1.id, "Test Task", None, TaskStatus::Todo).await; - let auth = AuthUser { - user_id: user.id, - email: user.email, - is_admin: false, - }; - // Try to get task from wrong project - let result = get_task(auth, Path((project2.id, task.id)), Extension(pool)).await; + let result = get_task(Path((project2.id, task.id)), Extension(pool)).await; assert!(result.is_err()); assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND); } @@ -673,25 +627,13 @@ mod tests { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let project = create_test_project(&pool, "Test Project", user.id).await; - let auth = AuthUser { - user_id: user.id, - email: user.email, - is_admin: false, - }; - let create_request = CreateTask { project_id: project.id, // This will be overridden by the path parameter title: "New Task".to_string(), 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), Extension(pool), Json(create_request)).await; assert!(result.is_ok()); let response = result.unwrap().0; @@ -712,12 +654,6 @@ mod tests { let user = create_test_user(&pool, "test@example.com", "password123", false).await; let nonexistent_project_id = Uuid::new_v4(); - let auth = AuthUser { - user_id: user.id, - email: user.email, - is_admin: false, - }; - let create_request = CreateTask { project_id: nonexistent_project_id, title: "New Task".to_string(), @@ -726,7 +662,6 @@ mod tests { let result = create_task( Path(nonexistent_project_id), - auth, Extension(pool), Json(create_request), ) diff --git a/backend/src/routes/users.rs b/backend/src/routes/users.rs deleted file mode 100644 index 6afc5319..00000000 --- a/backend/src/routes/users.rs +++ /dev/null @@ -1,607 +0,0 @@ -use axum::{ - extract::{Extension, Path}, - http::StatusCode, - response::Json as ResponseJson, - routing::{get, post}, - Json, Router, -}; -use sqlx::PgPool; -use uuid::Uuid; - -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, -) -> 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), - 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); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } -} - -pub async fn get_users( - _auth: AuthUser, - Extension(pool): Extension, -) -> Result>>, StatusCode> { - match User::find_all(&pool).await { - Ok(users) => { - let user_responses: Vec = users.into_iter().map(|u| u.into()).collect(); - Ok(ResponseJson(ApiResponse { - success: true, - data: Some(user_responses), - message: None, - })) - } - Err(e) => { - tracing::error!("Failed to fetch users: {}", e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } -} - -pub async fn get_user( - auth: AuthUser, - Path(id): Path, - Extension(pool): Extension, -) -> Result>, StatusCode> { - // Users can only view their own profile unless they're admin - if auth.user_id != id && !auth.is_admin { - return Err(StatusCode::FORBIDDEN); - } - - match User::find_by_id(&pool, id).await { - Ok(Some(user)) => Ok(ResponseJson(ApiResponse { - success: true, - data: Some(user.into()), - message: None, - })), - Ok(None) => Err(StatusCode::NOT_FOUND), - Err(e) => { - tracing::error!("Failed to fetch user: {}", e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } -} - -pub async fn create_user( - auth: AuthUser, - Extension(pool): Extension, - Json(payload): Json, -) -> Result>, StatusCode> { - // Only admins can create users - if !auth.is_admin { - return Err(StatusCode::FORBIDDEN); - } - - let id = Uuid::new_v4(); - - let password_hash = match hash_password(&payload.password) { - Ok(hash) => hash, - Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), - }; - - match User::create(&pool, &payload, password_hash, id).await { - Ok(user) => Ok(ResponseJson(ApiResponse { - success: true, - data: Some(user.into()), - message: Some("User created successfully".to_string()), - })), - Err(e) => { - tracing::error!("Failed to create user: {}", e); - if e.to_string().contains("users_email_key") { - Err(StatusCode::CONFLICT) // Email already exists - } else { - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } -} - -pub async fn update_user( - auth: AuthUser, - Path(id): Path, - Extension(pool): Extension, - Json(payload): Json, -) -> Result>, StatusCode> { - // Users can only update their own profile unless they're admin - if auth.user_id != id && !auth.is_admin { - return Err(StatusCode::FORBIDDEN); - } - - // Get existing user - let existing_user = match User::find_by_id(&pool, id).await { - Ok(Some(user)) => user, - Ok(None) => return Err(StatusCode::NOT_FOUND), - Err(e) => { - tracing::error!("Failed to check user existence: {}", e); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; - - let email = payload.email.unwrap_or(existing_user.email); - let is_admin = if auth.is_admin { - payload.is_admin.unwrap_or(existing_user.is_admin) - } else { - existing_user.is_admin // Non-admins can't change admin status - }; - - let password_hash = if let Some(new_password) = payload.password { - match hash_password(&new_password) { - Ok(hash) => hash, - Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), - } - } else { - existing_user.password_hash - }; - - match User::update(&pool, id, email, password_hash, is_admin).await { - Ok(user) => Ok(ResponseJson(ApiResponse { - success: true, - data: Some(user.into()), - message: Some("User updated successfully".to_string()), - })), - Err(e) => { - tracing::error!("Failed to update user: {}", e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } -} - -pub async fn delete_user( - auth: AuthUser, - Path(id): Path, - Extension(pool): Extension, -) -> Result>, StatusCode> { - // Only admins can delete users, and they can't delete themselves - if !auth.is_admin || auth.user_id == id { - return Err(StatusCode::FORBIDDEN); - } - - match User::delete(&pool, id).await { - Ok(rows_affected) => { - if rows_affected == 0 { - Err(StatusCode::NOT_FOUND) - } else { - Ok(ResponseJson(ApiResponse { - success: true, - data: None, - message: Some("User deleted successfully".to_string()), - })) - } - } - Err(e) => { - tracing::error!("Failed to delete user: {}", e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } -} - -pub async fn get_current_user( - auth: AuthUser, - Extension(pool): Extension, -) -> Result>, StatusCode> { - match User::find_by_id(&pool, auth.user_id).await { - Ok(Some(user)) => Ok(ResponseJson(ApiResponse { - success: true, - data: Some(user.into()), - message: None, - })), - Ok(None) => Err(StatusCode::NOT_FOUND), - Err(e) => { - tracing::error!("Failed to fetch current user: {}", e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } -} - -pub async fn check_auth_status(auth: AuthUser) -> ResponseJson> { - ResponseJson(ApiResponse { - success: true, - data: Some(serde_json::json!({ - "authenticated": true, - "user_id": auth.user_id, - "email": auth.email, - "is_admin": auth.is_admin - })), - message: Some("User is authenticated".to_string()), - }) -} - -pub fn public_users_router() -> Router { - Router::new().route("/auth/login", post(login)) -} - -pub fn protected_users_router() -> Router { - Router::new() - .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), - ) -} - -#[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; - - async fn create_test_user(pool: &PgPool, email: &str, password: &str, is_admin: bool) -> User { - let id = Uuid::new_v4(); - let now = Utc::now(); - let password_hash = hash_password(password).unwrap(); - - sqlx::query_as!( - User, - "INSERT INTO users (id, email, password_hash, is_admin, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, email, password_hash, is_admin, created_at, updated_at", - id, - email, - password_hash, - is_admin, - now, - now - ) - .fetch_one(pool) - .await - .unwrap() - } - - #[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(), - }; - - 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()); - assert_eq!(response.data.as_ref().unwrap().user.email, user.email); - } - - #[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(), - }; - - let result = login(Extension(pool), Json(login_request)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::UNAUTHORIZED); - } - - #[sqlx::test] - async fn test_login_user_not_found(pool: PgPool) { - let login_request = LoginRequest { - email: "nonexistent@example.com".to_string(), - password: "password123".to_string(), - }; - - let result = login(Extension(pool), Json(login_request)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::UNAUTHORIZED); - } - - #[sqlx::test] - async fn test_get_users_as_admin(pool: PgPool) { - let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; - create_test_user(&pool, "user1@example.com", "password123", false).await; - create_test_user(&pool, "user2@example.com", "password123", false).await; - - let auth = AuthUser { - user_id: admin_user.id, - email: admin_user.email, - is_admin: true, - }; - - 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()); - assert_eq!(response.data.unwrap().len(), 3); - } - - #[sqlx::test] - async fn test_get_user_own_profile(pool: PgPool) { - let user = create_test_user(&pool, "test@example.com", "password123", false).await; - - let auth = AuthUser { - user_id: user.id, - email: user.email.clone(), - is_admin: false, - }; - - 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()); - assert_eq!(response.data.unwrap().email, user.email); - } - - #[sqlx::test] - async fn test_get_user_forbidden_non_admin(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 auth = AuthUser { - user_id: user1.id, - email: user1.email, - is_admin: false, - }; - - let result = get_user(auth, Path(user2.id), Extension(pool)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); - } - - #[sqlx::test] - async fn test_get_user_admin_can_view_any(pool: PgPool) { - let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; - let regular_user = create_test_user(&pool, "user@example.com", "password123", false).await; - - let auth = AuthUser { - user_id: admin_user.id, - email: admin_user.email, - is_admin: true, - }; - - 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()); - assert_eq!(response.data.unwrap().email, regular_user.email); - } - - #[sqlx::test] - async fn test_create_user_as_admin(pool: PgPool) { - let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; - - let auth = AuthUser { - user_id: admin_user.id, - email: admin_user.email, - is_admin: true, - }; - - let create_request = CreateUser { - email: "newuser@example.com".to_string(), - password: "password123".to_string(), - is_admin: Some(false), - }; - - 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()); - assert_eq!(response.data.unwrap().email, "newuser@example.com"); - } - - #[sqlx::test] - async fn test_create_user_forbidden_non_admin(pool: PgPool) { - let regular_user = create_test_user(&pool, "user@example.com", "password123", false).await; - - let auth = AuthUser { - user_id: regular_user.id, - email: regular_user.email, - is_admin: false, - }; - - let create_request = CreateUser { - email: "newuser@example.com".to_string(), - password: "password123".to_string(), - is_admin: Some(false), - }; - - let result = create_user(auth, Extension(pool), Json(create_request)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); - } - - #[sqlx::test] - async fn test_create_user_duplicate_email(pool: PgPool) { - let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; - create_test_user(&pool, "existing@example.com", "password123", false).await; - - let auth = AuthUser { - user_id: admin_user.id, - email: admin_user.email, - is_admin: true, - }; - - let create_request = CreateUser { - email: "existing@example.com".to_string(), - password: "password123".to_string(), - is_admin: Some(false), - }; - - let result = create_user(auth, Extension(pool), Json(create_request)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::CONFLICT); - } - - #[sqlx::test] - async fn test_update_user_own_profile(pool: PgPool) { - let user = create_test_user(&pool, "user@example.com", "password123", false).await; - - let auth = AuthUser { - user_id: user.id, - email: user.email.clone(), - is_admin: false, - }; - - let update_request = UpdateUser { - email: Some("newemail@example.com".to_string()), - password: Some("newpassword123".to_string()), - is_admin: None, - }; - - 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()); - assert_eq!(response.data.unwrap().email, "newemail@example.com"); - } - - #[sqlx::test] - async fn test_update_user_forbidden_non_admin(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 auth = AuthUser { - user_id: user1.id, - email: user1.email, - is_admin: false, - }; - - let update_request = UpdateUser { - email: Some("newemail@example.com".to_string()), - password: None, - is_admin: None, - }; - - let result = update_user(auth, Path(user2.id), Extension(pool), Json(update_request)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); - } - - #[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 auth = AuthUser { - user_id: admin_user.id, - email: admin_user.email, - is_admin: true, - }; - - 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"); - } - - #[sqlx::test] - async fn test_delete_user_forbidden_non_admin(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 auth = AuthUser { - user_id: user1.id, - email: user1.email, - is_admin: false, - }; - - let result = delete_user(auth, Path(user2.id), Extension(pool)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); - } - - #[sqlx::test] - async fn test_delete_user_self_forbidden(pool: PgPool) { - let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; - - let auth = AuthUser { - user_id: admin_user.id, - email: admin_user.email.clone(), - is_admin: true, - }; - - let result = delete_user(auth, Path(admin_user.id), Extension(pool)).await; - assert!(result.is_err()); - assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); - } - - #[sqlx::test] - async fn test_get_current_user(pool: PgPool) { - let user = create_test_user(&pool, "user@example.com", "password123", false).await; - - let auth = AuthUser { - user_id: user.id, - email: user.email.clone(), - is_admin: false, - }; - - 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()); - assert_eq!(response.data.unwrap().email, user.email); - } - - #[tokio::test] - async fn test_check_auth_status() { - let auth = AuthUser { - user_id: Uuid::new_v4(), - email: "test@example.com".to_string(), - is_admin: true, - }; - - 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()); - assert_eq!(data["email"], auth.email); - assert_eq!(data["is_admin"], auth.is_admin); - } -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5d37d3af..77cc33a1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,43 +1,19 @@ import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom' -import { LoginForm } from '@/components/auth/login-form' import { Navbar } from '@/components/layout/navbar' import { HomePage } from '@/pages/home' import { Projects } from '@/pages/projects' import { ProjectTasks } from '@/pages/project-tasks' import { TaskDetailsPage } from '@/pages/task-details' import { TaskAttemptComparePage } from '@/pages/task-attempt-compare' -import { Users } from '@/pages/users' -import { AuthProvider, useAuth } from '@/contexts/auth-context' + function AppContent() { const location = useLocation() - const { isAuthenticated, isLoading, logout } = useAuth() - const showNavbar = location.pathname !== '/' || isAuthenticated - - const handleLogin = () => { - // The actual login logic is handled by the LoginForm component - // which will call the login method from useAuth() - } - - // Show loading while checking auth status - if (isLoading) { - return ( -
-
-
-

Checking authentication...

-
-
- ) - } - - if (!isAuthenticated) { - return - } + const showNavbar = true return (
- {showNavbar && } + {showNavbar && }
} /> @@ -46,7 +22,7 @@ function AppContent() { } /> } /> } /> - } /> +
@@ -56,9 +32,7 @@ function AppContent() { function App() { return ( - - - + ) } diff --git a/frontend/src/components/auth/login-form.tsx b/frontend/src/components/auth/login-form.tsx deleted file mode 100644 index a320397f..00000000 --- a/frontend/src/components/auth/login-form.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useState } from 'react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Alert, AlertDescription } from '@/components/ui/alert' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { LoginRequest, LoginResponse, ApiResponse } from 'shared/types' -import { useAuth } from '@/contexts/auth-context' -import { LogIn, AlertCircle } from 'lucide-react' - -interface LoginFormProps { - onSuccess?: () => void -} - -export function LoginForm({ onSuccess }: LoginFormProps) { - const { login } = useAuth() - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [loading, setLoading] = useState(false) - const [error, setError] = useState('') - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError('') - setLoading(true) - - try { - const loginData: LoginRequest = { email, password } - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(loginData), - }) - - if (!response.ok) { - if (response.status === 401) { - throw new Error('Invalid email or password') - } - throw new Error('Login failed') - } - - const data: ApiResponse = await response.json() - - if (data.success && data.data) { - login(data.data.user, data.data.token) - onSuccess?.() - } else { - throw new Error('Login failed') - } - } catch (error) { - setError(error instanceof Error ? error.message : 'An error occurred') - } finally { - setLoading(false) - } - } - - return ( -
- - -
- -
- Welcome back - - Sign in to your account to continue - -
- -
-
- - setEmail(e.target.value)} - placeholder="Enter your email" - required - /> -
- -
- - setPassword(e.target.value)} - placeholder="Enter your password" - required - /> -
- - {error && ( - - - - {error} - - - )} - - -
- -
-

Default admin credentials:

-

Email: admin@example.com

-

Password: Check your ADMIN_PASSWORD env var

-
-
-
-
- ) -} diff --git a/frontend/src/components/layout/navbar.tsx b/frontend/src/components/layout/navbar.tsx index 14445b99..04ed5729 100644 --- a/frontend/src/components/layout/navbar.tsx +++ b/frontend/src/components/layout/navbar.tsx @@ -1,21 +1,11 @@ import { Link, useLocation } from 'react-router-dom' import { Button } from '@/components/ui/button' -import { authStorage } from '@/lib/auth' -import { ArrowLeft, FolderOpen, Users, LogOut } from 'lucide-react' +import { ArrowLeft, FolderOpen, Users } from 'lucide-react' -interface NavbarProps { - onLogout: () => void -} - -export function Navbar({ onLogout }: NavbarProps) { +export function Navbar() { const location = useLocation() - const currentUser = authStorage.getUser() const isHome = location.pathname === '/' - const handleLogout = () => { - onLogout() - } - return (
@@ -33,24 +23,19 @@ export function Navbar({ onLogout }: NavbarProps) { Projects - {currentUser?.is_admin && ( - - )} +
-
- Welcome, {currentUser?.email} -
{!isHome && ( )} -
diff --git a/frontend/src/components/projects/project-detail.tsx b/frontend/src/components/projects/project-detail.tsx index cc343ba6..9a26183a 100644 --- a/frontend/src/components/projects/project-detail.tsx +++ b/frontend/src/components/projects/project-detail.tsx @@ -6,8 +6,8 @@ import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' import { Project, ApiResponse } from 'shared/types' import { ProjectForm } from './project-form' -import { makeAuthenticatedRequest } from '@/lib/auth' -import { ArrowLeft, Edit, Trash2, Calendar, Clock, User, AlertCircle, Loader2, CheckSquare } from 'lucide-react' +import { makeRequest } from '@/lib/api' +import { ArrowLeft, Edit, Trash2, Calendar, Clock, AlertCircle, Loader2, CheckSquare } from 'lucide-react' interface ProjectDetailProps { projectId: string @@ -25,7 +25,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) { setLoading(true) setError('') try { - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`) + const response = await makeRequest(`/api/projects/${projectId}`) const data: ApiResponse = await response.json() if (data.success && data.data) { setProject(data.data) @@ -45,7 +45,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) { if (!confirm(`Are you sure you want to delete "${project.name}"? This action cannot be undone.`)) return try { - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`, { + const response = await makeRequest(`/api/projects/${projectId}`, { method: 'DELETE', }) if (response.ok) { @@ -166,13 +166,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) { Last Updated: {new Date(project.updated_at).toLocaleDateString()} -
- - Owner ID: - - {project.owner_id.substring(0, 8)}... - -
+ diff --git a/frontend/src/components/projects/project-form.tsx b/frontend/src/components/projects/project-form.tsx index 40bf61af..994374ad 100644 --- a/frontend/src/components/projects/project-form.tsx +++ b/frontend/src/components/projects/project-form.tsx @@ -14,7 +14,7 @@ import { import { FolderPicker } from "@/components/ui/folder-picker"; import { Project, CreateProject, UpdateProject } from "shared/types"; import { AlertCircle, Folder } from "lucide-react"; -import { makeAuthenticatedRequest } from "@/lib/auth"; +import { makeRequest } from "@/lib/api"; interface ProjectFormProps { open: boolean; @@ -76,7 +76,7 @@ export function ProjectForm({ name, git_repo_path: finalGitRepoPath, }; - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${project.id}`, { method: "PUT", @@ -98,7 +98,7 @@ export function ProjectForm({ git_repo_path: finalGitRepoPath, use_existing_repo: repoMode === "existing", }; - const response = await makeAuthenticatedRequest("/api/projects", { + const response = await makeRequest("/api/projects", { method: "POST", body: JSON.stringify(createData), }); diff --git a/frontend/src/components/projects/project-list.tsx b/frontend/src/components/projects/project-list.tsx index e4634af7..9107634c 100644 --- a/frontend/src/components/projects/project-list.tsx +++ b/frontend/src/components/projects/project-list.tsx @@ -6,7 +6,7 @@ import { Badge } from '@/components/ui/badge' import { Alert, AlertDescription } from '@/components/ui/alert' import { Project, ApiResponse } from 'shared/types' import { ProjectForm } from './project-form' -import { makeAuthenticatedRequest } from '@/lib/auth' +import { makeRequest } from '@/lib/api' import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, MoreHorizontal, ExternalLink } from 'lucide-react' import { DropdownMenu, @@ -27,7 +27,7 @@ export function ProjectList() { setLoading(true) setError('') try { - const response = await makeAuthenticatedRequest('/api/projects') + const response = await makeRequest('/api/projects') const data: ApiResponse = await response.json() if (data.success && data.data) { setProjects(data.data) @@ -46,7 +46,7 @@ export function ProjectList() { if (!confirm(`Are you sure you want to delete "${name}"? This action cannot be undone.`)) return try { - const response = await makeAuthenticatedRequest(`/api/projects/${id}`, { + const response = await makeRequest(`/api/projects/${id}`, { method: 'DELETE', }) if (response.ok) { diff --git a/frontend/src/components/tasks/TaskDetailsDialog.tsx b/frontend/src/components/tasks/TaskDetailsDialog.tsx index ebae4add..dcab90e3 100644 --- a/frontend/src/components/tasks/TaskDetailsDialog.tsx +++ b/frontend/src/components/tasks/TaskDetailsDialog.tsx @@ -18,7 +18,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; -import { makeAuthenticatedRequest } from "@/lib/auth"; +import { makeRequest } from "@/lib/api"; import type { TaskStatus, TaskAttempt, @@ -107,7 +107,7 @@ export function TaskDetailsDialog({ const fetchTaskAttempts = async (taskId: string) => { try { setTaskAttemptsLoading(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${taskId}/attempts` ); @@ -141,7 +141,7 @@ export function TaskDetailsDialog({ try { setActivitiesLoading(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities` ); @@ -171,7 +171,7 @@ export function TaskDetailsDialog({ try { setSavingTask(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}`, { method: "PUT", @@ -216,7 +216,7 @@ export function TaskDetailsDialog({ setCreatingAttempt(true); const worktreePath = `/tmp/task-${task.id}-attempt-${Date.now()}`; - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts`, { method: "POST", @@ -251,7 +251,7 @@ export function TaskDetailsDialog({ try { setStoppingAttempt(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/stop`, { method: "POST", diff --git a/frontend/src/components/ui/folder-picker.tsx b/frontend/src/components/ui/folder-picker.tsx index 91e9e8ab..853539d8 100644 --- a/frontend/src/components/ui/folder-picker.tsx +++ b/frontend/src/components/ui/folder-picker.tsx @@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Alert, AlertDescription } from '@/components/ui/alert' import { Folder, FolderOpen, File, AlertCircle, Home, ChevronUp } from 'lucide-react' -import { makeAuthenticatedRequest } from '@/lib/auth' +import { makeRequest } from '@/lib/api' import { DirectoryEntry } from 'shared/types' interface FolderPickerProps { @@ -43,7 +43,7 @@ export function FolderPicker({ try { const queryParam = path ? `?path=${encodeURIComponent(path)}` : '' - const response = await makeAuthenticatedRequest(`/api/filesystem/list${queryParam}`) + const response = await makeRequest(`/api/filesystem/list${queryParam}`) if (!response.ok) { throw new Error('Failed to load directory') diff --git a/frontend/src/components/users/user-form.tsx b/frontend/src/components/users/user-form.tsx deleted file mode 100644 index ff4dfa9f..00000000 --- a/frontend/src/components/users/user-form.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useState } from 'react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Alert, AlertDescription } from '@/components/ui/alert' -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { User, CreateUser, UpdateUser } from 'shared/types' -import { makeAuthenticatedRequest, authStorage } from '@/lib/auth' -import { AlertCircle } from 'lucide-react' - -interface UserFormProps { - open: boolean - onClose: () => void - onSuccess: () => void - user?: User | null -} - -export function UserForm({ open, onClose, onSuccess, user }: UserFormProps) { - const [email, setEmail] = useState(user?.email || '') - const [password, setPassword] = useState('') - const [isAdmin, setIsAdmin] = useState(user?.is_admin || false) - const [loading, setLoading] = useState(false) - const [error, setError] = useState('') - - const currentUser = authStorage.getUser() - const isEditing = !!user - const canEditAdminStatus = currentUser?.is_admin && currentUser.id !== user?.id - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError('') - setLoading(true) - - try { - if (isEditing) { - const updateData: UpdateUser = { - email: email !== user.email ? email : null, - password: password ? password : null, - is_admin: canEditAdminStatus && isAdmin !== user.is_admin ? isAdmin : null - } - - // Remove null values - Object.keys(updateData).forEach(key => { - if (updateData[key as keyof UpdateUser] === null) { - delete updateData[key as keyof UpdateUser] - } - }) - - const response = await makeAuthenticatedRequest(`/api/users/${user.id}`, { - method: 'PUT', - body: JSON.stringify(updateData), - }) - - if (!response.ok) { - throw new Error('Failed to update user') - } - } else { - if (!password) { - throw new Error('Password is required for new users') - } - - const createData: CreateUser = { - email, - password, - is_admin: currentUser?.is_admin ? isAdmin : false - } - - const response = await makeAuthenticatedRequest('/api/users', { - method: 'POST', - body: JSON.stringify(createData), - }) - - if (!response.ok) { - if (response.status === 409) { - throw new Error('A user with this email already exists') - } - throw new Error('Failed to create user') - } - } - - onSuccess() - resetForm() - } catch (error) { - setError(error instanceof Error ? error.message : 'An error occurred') - } finally { - setLoading(false) - } - } - - const resetForm = () => { - setEmail(user?.email || '') - setPassword('') - setIsAdmin(user?.is_admin || false) - setError('') - } - - const handleClose = () => { - resetForm() - onClose() - } - - return ( - - - - - {isEditing ? 'Edit User' : 'Create New User'} - - - {isEditing - ? 'Make changes to the user account here. Click save when you\'re done.' - : 'Add a new user to the system. They will be able to log in with these credentials.' - } - - - -
-
- - setEmail(e.target.value)} - placeholder="Enter email address" - required - /> -
- -
- - setPassword(e.target.value)} - placeholder={isEditing ? "Enter new password" : "Enter password"} - required={!isEditing} - /> -
- - {canEditAdminStatus && ( -
- setIsAdmin(e.target.checked)} - className="rounded border-gray-300" - /> - -
- )} - - {error && ( - - - - {error} - - - )} - - - - - -
-
-
- ) -} diff --git a/frontend/src/components/users/user-list.tsx b/frontend/src/components/users/user-list.tsx deleted file mode 100644 index 4a239464..00000000 --- a/frontend/src/components/users/user-list.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { useState, useEffect } from 'react' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' -import { Alert, AlertDescription } from '@/components/ui/alert' -import { User, ApiResponse } from 'shared/types' -import { UserForm } from './user-form' -import { makeAuthenticatedRequest, authStorage } from '@/lib/auth' -import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, Shield, User as UserIcon } from 'lucide-react' - -export function UserList() { - const [users, setUsers] = useState([]) - const [loading, setLoading] = useState(false) - const [showForm, setShowForm] = useState(false) - const [editingUser, setEditingUser] = useState(null) - const [error, setError] = useState('') - const currentUser = authStorage.getUser() - - const fetchUsers = async () => { - setLoading(true) - setError('') - try { - const response = await makeAuthenticatedRequest('/api/users') - const data: ApiResponse = await response.json() - if (data.success && data.data) { - setUsers(data.data) - } else { - setError('Failed to load users') - } - } catch (error) { - console.error('Failed to fetch users:', error) - setError('Failed to connect to server') - } finally { - setLoading(false) - } - } - - const handleDelete = async (id: string, email: string) => { - if (!confirm(`Are you sure you want to delete user "${email}"? This action cannot be undone.`)) return - - try { - const response = await makeAuthenticatedRequest(`/api/users/${id}`, { - method: 'DELETE', - }) - if (response.ok) { - fetchUsers() - } else if (response.status === 403) { - setError('You cannot delete this user') - } else { - setError('Failed to delete user') - } - } catch (error) { - console.error('Failed to delete user:', error) - setError('Failed to delete user') - } - } - - const handleEdit = (user: User) => { - setEditingUser(user) - setShowForm(true) - } - - const handleFormSuccess = () => { - setShowForm(false) - setEditingUser(null) - fetchUsers() - } - - useEffect(() => { - fetchUsers() - }, []) - - return ( -
-
-
-

Users

-

- Manage user accounts and permissions -

-
- {currentUser?.is_admin && ( - - )} -
- - {error && ( - - - - {error} - - - )} - - {loading ? ( -
- - Loading users... -
- ) : users.length === 0 ? ( - - -
- -
-

No users found

-

- Get started by creating the first user account. -

- {currentUser?.is_admin && ( - - )} -
-
- ) : ( -
- {users.map((user) => ( - - -
- - {user.is_admin ? ( - - ) : ( - - )} - {user.email} - - - {user.is_admin ? "Admin" : "User"} - -
- - - Joined {user.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'} - -
- -
- - {currentUser?.is_admin && currentUser.id !== user.id && ( - - )} -
-
-
- ))} -
- )} - - { - setShowForm(false) - setEditingUser(null) - }} - onSuccess={handleFormSuccess} - user={editingUser} - /> -
- ) -} diff --git a/frontend/src/components/users/users-page.tsx b/frontend/src/components/users/users-page.tsx deleted file mode 100644 index 92392027..00000000 --- a/frontend/src/components/users/users-page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { UserList } from './user-list' - -export function UsersPage() { - return -} diff --git a/frontend/src/contexts/auth-context.tsx b/frontend/src/contexts/auth-context.tsx deleted file mode 100644 index 5cd71c3e..00000000 --- a/frontend/src/contexts/auth-context.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { createContext, useContext, useState, useEffect, ReactNode } from 'react' -import { isAuthenticated, authStorage, makeAuthenticatedRequest } from '@/lib/auth' -import { User } from 'shared/types' - -interface AuthContextType { - user: User | null - isAuthenticated: boolean - isLoading: boolean - login: (user: User, token: string) => void - logout: () => void - refreshAuthStatus: () => Promise -} - -const AuthContext = createContext(undefined) - -interface AuthProviderProps { - children: ReactNode -} - -export function AuthProvider({ children }: AuthProviderProps) { - const [user, setUser] = useState(null) - const [isAuthenticatedState, setIsAuthenticated] = useState(false) - const [isLoading, setIsLoading] = useState(true) - - const checkAuthStatus = async (): Promise => { - const token = authStorage.getToken() - if (!token) { - return false - } - - try { - const response = await makeAuthenticatedRequest('/api/auth/status') - - if (response.ok) { - const data = await response.json() - if (data.success && data.data?.authenticated) { - // Update user data from server response - if (data.data.user_id && data.data.email) { - const userData: User = { - id: data.data.user_id, - email: data.data.email, - is_admin: data.data.is_admin || false, - created_at: new Date(), - updated_at: new Date() - } - authStorage.setUser(userData) - setUser(userData) - } - return true - } - } - - // If we get here, the token is invalid - return false - } catch (error) { - console.error('Auth status check failed:', error) - return false - } - } - - const refreshAuthStatus = async () => { - setIsLoading(true) - - if (isAuthenticated()) { - const isValid = await checkAuthStatus() - if (isValid) { - setIsAuthenticated(true) - } else { - // Clear invalid auth state - authStorage.clear() - setUser(null) - setIsAuthenticated(false) - } - } else { - setUser(null) - setIsAuthenticated(false) - } - - setIsLoading(false) - } - - useEffect(() => { - refreshAuthStatus() - }, []) - - const login = (userData: User, token: string) => { - authStorage.setToken(token) - authStorage.setUser(userData) - setUser(userData) - setIsAuthenticated(true) - } - - const logout = () => { - authStorage.clear() - setUser(null) - setIsAuthenticated(false) - window.location.href = '/' - } - - const value: AuthContextType = { - user, - isAuthenticated: isAuthenticatedState, - isLoading, - login, - logout, - refreshAuthStatus - } - - return ( - - {children} - - ) -} - -export function useAuth() { - const context = useContext(AuthContext) - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider') - } - return context -} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 00000000..3da99727 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,11 @@ +export const makeRequest = async (url: string, options: RequestInit = {}) => { + const headers = { + 'Content-Type': 'application/json', + ...(options.headers || {}) + } + + return fetch(url, { + ...options, + headers + }) +} diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts deleted file mode 100644 index 5d5b1f6f..00000000 --- a/frontend/src/lib/auth.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { User } from 'shared/types' - -const TOKEN_KEY = 'auth_token' -const USER_KEY = 'auth_user' - -export const authStorage = { - getToken: (): string | null => { - return localStorage.getItem(TOKEN_KEY) - }, - - setToken: (token: string): void => { - localStorage.setItem(TOKEN_KEY, token) - }, - - removeToken: (): void => { - localStorage.removeItem(TOKEN_KEY) - }, - - getUser: (): User | null => { - const user = localStorage.getItem(USER_KEY) - return user ? JSON.parse(user) : null - }, - - setUser: (user: User): void => { - localStorage.setItem(USER_KEY, JSON.stringify(user)) - }, - - removeUser: (): void => { - localStorage.removeItem(USER_KEY) - }, - - clear: (): void => { - localStorage.removeItem(TOKEN_KEY) - localStorage.removeItem(USER_KEY) - } -} - -export const getAuthHeaders = (): Record => { - const token = authStorage.getToken() - return token ? { Authorization: `Bearer ${token}` } : {} -} - -export const makeAuthenticatedRequest = async (url: string, options: RequestInit = {}) => { - const headers = { - 'Content-Type': 'application/json', - ...getAuthHeaders(), - ...(options.headers || {}) - } - - return fetch(url, { - ...options, - headers - }) -} - -export const isAuthenticated = (): boolean => { - return !!authStorage.getToken() -} diff --git a/frontend/src/pages/home.tsx b/frontend/src/pages/home.tsx index 3cdc5f15..35b6f0dd 100644 --- a/frontend/src/pages/home.tsx +++ b/frontend/src/pages/home.tsx @@ -11,7 +11,7 @@ import { import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { ApiResponse } from "shared/types"; -import { authStorage, makeAuthenticatedRequest } from "@/lib/auth"; +import { makeRequest } from "@/lib/api"; import { Heart, Activity, @@ -20,7 +20,6 @@ import { CheckCircle, AlertCircle, Zap, - Shield, } from "lucide-react"; export function HomePage() { @@ -30,12 +29,12 @@ export function HomePage() { ); const [loading, setLoading] = useState(false); - const currentUser = authStorage.getUser(); + // Single user app, no need for user data const checkHealth = async () => { setLoading(true); try { - const response = await makeAuthenticatedRequest("/api/health"); + const response = await makeRequest("/api/health"); const data: ApiResponse = await response.json(); setMessage(data.message || "Health check completed"); setMessageType("success"); @@ -134,43 +133,37 @@ export function HomePage() { - {currentUser?.is_admin && ( - - -
-
-
- -
-
- - Users - - - Admin Only - - - - Manage user accounts and permissions - -
+ + +
+
+
+ +
+
+ + Users + + + Manage user accounts and permissions +
- - - - - - )} +
+
+ + + +
{/* Status Alert */} diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index 20696de6..76609e58 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { ArrowLeft, Plus } from 'lucide-react' -import { makeAuthenticatedRequest } from '@/lib/auth' +import { makeRequest } from '@/lib/api' import { TaskCreateDialog } from '@/components/tasks/TaskCreateDialog' import { TaskEditDialog } from '@/components/tasks/TaskEditDialog' @@ -53,7 +53,7 @@ export function ProjectTasks() { const fetchProject = async () => { try { - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}`) + const response = await makeRequest(`/api/projects/${projectId}`) if (response.ok) { const result: ApiResponse = await response.json() @@ -72,7 +72,7 @@ export function ProjectTasks() { const fetchTasks = async () => { try { setLoading(true) - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`) + const response = await makeRequest(`/api/projects/${projectId}/tasks`) if (response.ok) { const result: ApiResponse = await response.json() @@ -91,7 +91,7 @@ export function ProjectTasks() { const handleCreateTask = async (title: string, description: string) => { try { - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks`, { + const response = await makeRequest(`/api/projects/${projectId}/tasks`, { method: 'POST', body: JSON.stringify({ project_id: projectId, @@ -114,7 +114,7 @@ export function ProjectTasks() { if (!editingTask) return try { - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${editingTask.id}`, { + const response = await makeRequest(`/api/projects/${projectId}/tasks/${editingTask.id}`, { method: 'PUT', body: JSON.stringify({ title, @@ -138,7 +138,7 @@ export function ProjectTasks() { if (!confirm('Are you sure you want to delete this task?')) return try { - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}`, { + const response = await makeRequest(`/api/projects/${projectId}/tasks/${taskId}`, { method: 'DELETE', }) @@ -179,7 +179,7 @@ export function ProjectTasks() { )) try { - const response = await makeAuthenticatedRequest(`/api/projects/${projectId}/tasks/${taskId}`, { + const response = await makeRequest(`/api/projects/${projectId}/tasks/${taskId}`, { method: 'PUT', body: JSON.stringify({ title: task.title, diff --git a/frontend/src/pages/task-attempt-compare.tsx b/frontend/src/pages/task-attempt-compare.tsx index ea1d4db8..4fdc50c1 100644 --- a/frontend/src/pages/task-attempt-compare.tsx +++ b/frontend/src/pages/task-attempt-compare.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { ArrowLeft, FileText } from "lucide-react"; -import { makeAuthenticatedRequest } from "@/lib/auth"; +import { makeRequest } from "@/lib/api"; import type { WorktreeDiff, DiffChunkType } from "shared/types"; interface ApiResponse { @@ -37,7 +37,7 @@ export function TaskAttemptComparePage() { try { setLoading(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/diff` ); @@ -67,7 +67,7 @@ export function TaskAttemptComparePage() { try { setMerging(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/merge`, { method: 'POST', diff --git a/frontend/src/pages/task-details.tsx b/frontend/src/pages/task-details.tsx index 6ff959e4..7454609c 100644 --- a/frontend/src/pages/task-details.tsx +++ b/frontend/src/pages/task-details.tsx @@ -14,7 +14,7 @@ import { } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { ArrowLeft, FileText } from "lucide-react"; -import { makeAuthenticatedRequest } from "@/lib/auth"; +import { makeRequest } from "@/lib/api"; import type { TaskStatus, TaskAttempt, @@ -119,7 +119,7 @@ export function TaskDetailsPage() { try { setTaskLoading(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${taskId}` ); @@ -152,7 +152,7 @@ export function TaskDetailsPage() { setTaskAttemptsLoading(true); } - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${taskId}/attempts` ); @@ -205,7 +205,7 @@ export function TaskDetailsPage() { setActivitiesLoading(true); } - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities` ); @@ -237,7 +237,7 @@ export function TaskDetailsPage() { try { setSavingTask(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}`, { method: "PUT", @@ -287,7 +287,7 @@ export function TaskDetailsPage() { setCreatingAttempt(true); const worktreePath = `/tmp/task-${task.id}-attempt-${Date.now()}`; - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts`, { method: "POST", @@ -322,7 +322,7 @@ export function TaskDetailsPage() { try { setStoppingAttempt(true); - const response = await makeAuthenticatedRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/stop`, { method: "POST", diff --git a/frontend/src/pages/users.tsx b/frontend/src/pages/users.tsx deleted file mode 100644 index 7b9d63c3..00000000 --- a/frontend/src/pages/users.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { useState, useEffect } from 'react' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' -import { Alert, AlertDescription } from '@/components/ui/alert' -import { User, ApiResponse } from 'shared/types' -import { UserForm } from '@/components/users/user-form' -import { makeAuthenticatedRequest, authStorage } from '@/lib/auth' -import { Plus, Edit, Trash2, Calendar, AlertCircle, Loader2, Shield, User as UserIcon } from 'lucide-react' - -export function Users() { - const [users, setUsers] = useState([]) - const [loading, setLoading] = useState(false) - const [showForm, setShowForm] = useState(false) - const [editingUser, setEditingUser] = useState(null) - const [error, setError] = useState('') - const currentUser = authStorage.getUser() - - const fetchUsers = async () => { - setLoading(true) - setError('') - try { - const response = await makeAuthenticatedRequest('/api/users') - const data: ApiResponse = await response.json() - if (data.success && data.data) { - setUsers(data.data) - } else { - setError('Failed to load users') - } - } catch (error) { - console.error('Failed to fetch users:', error) - setError('Failed to connect to server') - } finally { - setLoading(false) - } - } - - const handleDelete = async (id: string, email: string) => { - if (!confirm(`Are you sure you want to delete user "${email}"? This action cannot be undone.`)) return - - try { - const response = await makeAuthenticatedRequest(`/api/users/${id}`, { - method: 'DELETE', - }) - if (response.ok) { - fetchUsers() - } else if (response.status === 403) { - setError('You cannot delete this user') - } else { - setError('Failed to delete user') - } - } catch (error) { - console.error('Failed to delete user:', error) - setError('Failed to delete user') - } - } - - const handleEdit = (user: User) => { - setEditingUser(user) - setShowForm(true) - } - - const handleFormSuccess = () => { - setShowForm(false) - setEditingUser(null) - fetchUsers() - } - - useEffect(() => { - fetchUsers() - }, []) - - return ( -
-
-
-

Users

-

- Manage user accounts and permissions -

-
- {currentUser?.is_admin && ( - - )} -
- - {error && ( - - - - {error} - - - )} - - {loading ? ( -
- - Loading users... -
- ) : users.length === 0 ? ( - - -
- -
-

No users found

-

- Get started by creating the first user account. -

- {currentUser?.is_admin && ( - - )} -
-
- ) : ( -
- {users.map((user) => ( - - -
- - {user.is_admin ? ( - - ) : ( - - )} - {user.email} - - - {user.is_admin ? "Admin" : "User"} - -
- - - Joined {user.created_at ? new Date(user.created_at).toLocaleDateString() : 'Unknown'} - -
- -
- - {currentUser?.is_admin && currentUser.id !== user.id && ( - - )} -
-
-
- ))} -
- )} - - { - setShowForm(false) - setEditingUser(null) - }} - onSuccess={handleFormSuccess} - user={editingUser} - /> -
- ) -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 48fae988..929f07a1 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -15,7 +15,6 @@ export default defineConfig({ '/api': { target: 'http://localhost:3001', changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, diff --git a/shared/types.ts b/shared/types.ts index 92ccedec..df35456c 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -7,7 +7,7 @@ export type ExecutorConfig = { "type": "echo" } | { "type": "claude" }; export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, }; -export type Project = { id: string, name: string, git_repo_path: string, owner_id: string, created_at: Date, updated_at: Date, }; +export type Project = { id: string, name: string, git_repo_path: string, created_at: Date, updated_at: Date, }; export type UpdateProject = { name: string | null, git_repo_path: string | null, }; @@ -33,16 +33,6 @@ export type TaskAttemptActivity = { id: string, task_attempt_id: string, status: export type CreateTaskAttemptActivity = { task_attempt_id: string, status: TaskAttemptStatus | null, note: string | null, }; -export type CreateUser = { email: string, password: string, is_admin: boolean | null, }; - -export type LoginRequest = { email: string, password: string, }; - -export type LoginResponse = { user: User, token: string, }; - -export type UpdateUser = { email: string | null, password: string | null, is_admin: boolean | null, }; - -export type User = { id: string, email: string, is_admin: boolean, created_at: Date, updated_at: Date, }; - export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, }; export type DiffChunkType = "Equal" | "Insert" | "Delete";