From 8d05d569defd8940772215d68fc0b0e0e5103973 Mon Sep 17 00:00:00 2001 From: Yashb404 Date: Fri, 30 Jan 2026 07:03:34 +0000 Subject: [PATCH 1/2] -implement delete functionality -implement delete button with strict check --- client/src/pages/dashboard.rs | 58 ++++++++++++++++++++++++++++-- client/style.css | 23 ++++++++++++ server/src/handlers/project.rs | 66 ++++++++++++++++++++++++++++++++-- server/src/router.rs | 2 +- 4 files changed, 143 insertions(+), 6 deletions(-) diff --git a/client/src/pages/dashboard.rs b/client/src/pages/dashboard.rs index d475905..2da4007 100644 --- a/client/src/pages/dashboard.rs +++ b/client/src/pages/dashboard.rs @@ -201,7 +201,7 @@ pub fn DashboardPage() -> impl IntoView {
-
+ @@ -336,8 +337,52 @@ fn DashboardProjectList( set_error: WriteSignal>, set_loading: WriteSignal, projects: ReadSignal>, + set_projects: WriteSignal>, // <--- Receive Setter user_login: Rc, ) -> impl IntoView { + + // Handler for deletion logic + let handle_delete = move |slug: String| { + let prompt_text = format!( + "⚠️ DESTRUCTIVE ACTION\n\nThis will permanently delete the project '{}' and its Docker image.\n\nPlease type the project name to confirm:", + slug + ); + + // FIX: Match against Ok(Some(input)) to handle the Result wrapper + if let Ok(Some(input)) = window().prompt_with_message(&prompt_text) { + if input == slug { + let slug_clone = slug.clone(); + + spawn_local(async move { + let url = format!("{}/api/project/{}", api_base(), slug_clone); + + let req = Request::delete(&url) + .credentials(RequestCredentials::Include) + .send() + .await; + + match req { + Ok(resp) => { + if resp.ok() { + set_projects.update(|list| { + list.retain(|p| p.slug != slug_clone); + }); + let _ = window().alert_with_message("Project and Docker image deleted."); + } else { + let _ = window().alert_with_message("Failed to delete project. Check server logs."); + } + }, + Err(_) => { + let _ = window().alert_with_message("Network error occurred."); + } + } + }); + } else { + let _ = window().alert_with_message("Project name mismatch. Deletion cancelled."); + } + } + }; + view! { {move || match error.get() { Some(err) => view! { @@ -371,6 +416,8 @@ fn DashboardProjectList( key=|p| p.slug.clone() children=move |proj| { let login = user_login_clone.as_ref().clone(); + let delete_slug = proj.slug.clone(); + view! {
@@ -379,11 +426,16 @@ fn DashboardProjectList(

"Image: "{proj.image_tag.clone()}

-
} @@ -395,4 +447,4 @@ fn DashboardProjectList( } }} } -} +} \ No newline at end of file diff --git a/client/style.css b/client/style.css index cb61fb7..91316b0 100644 --- a/client/style.css +++ b/client/style.css @@ -799,3 +799,26 @@ body { .dot { opacity: 0.6; transition: opacity 0.2s; } .terminal-header:hover .dot { opacity: 1; } + + +.btn-danger { + padding: 8px 16px; + background: transparent; + color: var(--text-muted); + border: 1px solid var(--border); + border-radius: 6px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 0.9rem; + text-decoration: none; + margin-left: 8px; /* Gap between View and Delete */ +} + +.btn-danger:hover { + background: rgba(220, 38, 38, 0.1); /* Darker Red bg */ + border-color: #dc2626; + color: #dc2626; + box-shadow: 0 0 15px rgba(220, 38, 38, 0.2); + transform: translateY(-1px); +} \ No newline at end of file diff --git a/server/src/handlers/project.rs b/server/src/handlers/project.rs index f4eda3e..05952dc 100644 --- a/server/src/handlers/project.rs +++ b/server/src/handlers/project.rs @@ -1,12 +1,12 @@ use axum::{ extract::{Path, Query, State}, - routing::{get, post}, + routing::{get, post,delete}, Router, Json, http::StatusCode, }; use bollard::container::{CreateContainerOptions, Config}; use bollard::models::HostConfig; -use bollard::image::CommitContainerOptions; +use bollard::image::{CommitContainerOptions, RemoveImageOptions}; use tower_sessions::Session; use uuid::Uuid; use serde::Deserialize; @@ -22,6 +22,7 @@ pub fn routes() -> Router { Router::new() .route("/api/my-projects", get(list_user_projects)) .route("/api/project/:username/:slug", get(get_project)) + .route("/api/project/:slug", delete(delete_project)) // <--- New Route .route("/api/search-projects", get(search_projects)) .route("/api/publish", post(publish_handler)) } @@ -243,3 +244,64 @@ pub async fn get_project( "owner_id": owner_id }))) } + +pub async fn delete_project( + State(state): State, + session: Session, + Path(slug): Path, +) -> Result { + // 1. Authenticate User + let user: Option = session.get("user") + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Session Error: {}", e)))?; + + let user = user.ok_or((StatusCode::UNAUTHORIZED, "Unauthorized".to_string()))?; + + // 2. Fetch Image Tag & Verify Ownership (Before Deletion) + // We need the image tag to clean up Docker, and we need to verify ownership strictly. + let record: Option<(String,)> = sqlx::query_as( + "SELECT image_tag FROM projects WHERE slug = $1 AND owner_id = $2" + ) + .bind(&slug) + .bind(user.id) + .fetch_optional(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB Error: {}", e)))?; + + let image_tag = match record { + Some(r) => r.0, + None => return Err((StatusCode::NOT_FOUND, "Project not found or access denied".to_string())), + }; + + // 3. Delete from Database (Source of Truth) + let db_result = sqlx::query( + "DELETE FROM projects WHERE slug = $1 AND owner_id = $2" + ) + .bind(&slug) + .bind(user.id) + .execute(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB Delete Error: {}", e)))?; + + if db_result.rows_affected() == 0 { + // This is unlikely given step 2, but handles race conditions + return Err((StatusCode::NOT_FOUND, "Project not found".to_string())); + } + + // 4. Purge Docker Image + // We use force=true to kill any active containers using this image. + // We map errors but do NOT fail the request if Docker fails (e.g., image already manualy deleted). + let remove_opts = RemoveImageOptions { + force: true, // Force removal even if containers are running + noprune: false, + }; + + if let Err(e) = state.docker.remove_image(&image_tag, Some(remove_opts), None).await { + // Log it, but don't fail the request to the client, as the DB entry is already gone. + eprintln!("Warning: Failed to remove docker image {}: {}", image_tag, e); + } else { + println!("Cleaned up image: {}", image_tag); + } + + Ok(StatusCode::OK) +} \ No newline at end of file diff --git a/server/src/router.rs b/server/src/router.rs index 251a7d9..6069ecd 100644 --- a/server/src/router.rs +++ b/server/src/router.rs @@ -23,7 +23,7 @@ pub fn create_router(state: AppState) -> Result()?) - .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_methods([Method::GET, Method::POST, Method::OPTIONS,Method::DELETE])//add delete method .allow_headers([CONTENT_TYPE, AUTHORIZATION]) .allow_credentials(true) ) From 3cac225d4e24b4f6db75ecf5acdb8ba5523454a7 Mon Sep 17 00:00:00 2001 From: Yashb404 Date: Fri, 30 Jan 2026 07:38:50 +0000 Subject: [PATCH 2/2] add option for custom origin --- server/src/router.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src/router.rs b/server/src/router.rs index 6069ecd..8d2574b 100644 --- a/server/src/router.rs +++ b/server/src/router.rs @@ -16,14 +16,19 @@ pub fn create_router(state: AppState) -> Result()?) - .allow_methods([Method::GET, Method::POST, Method::OPTIONS,Method::DELETE])//add delete method + .allow_origin(frontend_url.parse::()?) + // 2. ALLOW DELETE METHOD + .allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS]) .allow_headers([CONTENT_TYPE, AUTHORIZATION]) .allow_credentials(true) ) @@ -31,4 +36,4 @@ pub fn create_router(state: AppState) -> Result