From e55d4bfbd039bf813dda9dfe527562a722d709cb Mon Sep 17 00:00:00 2001 From: Igor Zalutski Date: Wed, 5 Nov 2025 16:59:17 +0000 Subject: [PATCH 1/2] Agent task to add project delete button --- agent-tasks/project-deletion.md | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 agent-tasks/project-deletion.md diff --git a/agent-tasks/project-deletion.md b/agent-tasks/project-deletion.md new file mode 100644 index 000000000..54845d711 --- /dev/null +++ b/agent-tasks/project-deletion.md @@ -0,0 +1,58 @@ +# Project Deletion: Implementation Plan (Essentials) + +## Scope +- Add a Delete Project action on the Project Details screen. +- Add backend support to soft-delete a project. + +## Current State +- UI files: `projects.$projectid.tsx`, `projects.index.tsx`. +- UI API: `UI/src/api/orchestrator_projects.ts` has get/update; no delete. +- ServerFns: `UI/src/api/orchestrator_serverFunctions.ts` has `getProjectFn`, `getProjectsFn`, `updateProjectFn`. +- Backend routes: `backend/bootstrap/main.go` exposes GET, PUT for projects; no DELETE. +- Model: `Project` uses `gorm.Model` (soft-delete supported). + +## API +- Method: DELETE +- Path: `/api/projects/:project_id/` +- Headers: same as existing projects endpoints (Authorization, DIGGER_ORG_ID, DIGGER_USER_ID, DIGGER_ORG_SOURCE). +- Responses: 204 on success; 404 not found; 403 forbidden; 500 on error. + +## Backend Steps +1) Add route in `backend/bootstrap/main.go`: + - `projectsApiGroup.DELETE("/:project_id/", controllers.DeleteProjectApi)` +2) Implement `DeleteProjectApi` in `backend/controllers/projects.go`: + - Resolve org from headers; fetch project by `org.ID` and `project_id`. + - `models.DB.GormDB.Delete(&project)` to soft-delete. + - Return 204. +3) Migrations: none (soft-delete already present). + +## UI Steps +1) Add `deleteProject` to `UI/src/api/orchestrator_projects.ts` (DELETE request with existing headers). Return empty object on 204. +2) Add `deleteProjectFn` to `UI/src/api/orchestrator_serverFunctions.ts` wrapping `deleteProject`. +3) Update `projects.$projectid.tsx`: + - Import `useRouter`, `useToast`, `deleteProjectFn`, and `AlertDialog` components; add a destructive Delete button. + - On confirm: call `deleteProjectFn({ data: { projectId, organisationId, userId } })`, toast success, navigate to `/dashboard/projects`. + - On error: toast destructive. + +## Security +- Endpoint protected by existing API middleware; UI invokes via server function to keep secrets server-side. + +## Acceptance Criteria +- Delete button shows on Project Details; confirmation dialog appears. +- Confirm deletes the project (soft-delete), redirects to Projects list, and shows success toast. +- Deleted project no longer appears in list; details by ID returns 404. + +## Test Plan +- API: DELETE with valid headers → 204; repeat → 404; missing headers → 403. +- UI: Confirm delete redirects to list; failure shows error toast. + +## Estimate +- Backend: 1–2 hours; UI: ~1 hour; total: ~2–3 hours. + +## Pointers +- Routes: `backend/bootstrap/main.go` +- Controller: `backend/controllers/projects.go` +- Model: `backend/models/orgs.go` +- UI API: `UI/src/api/orchestrator_projects.ts` +- ServerFn: `UI/src/api/orchestrator_serverFunctions.ts` +- Screen: `UI/src/routes/_authenticated/_dashboard/dashboard/projects.$projectid.tsx` From cd4b9349f0671026c8983965332568e510b98572 Mon Sep 17 00:00:00 2001 From: Igor Zalutski Date: Wed, 5 Nov 2025 17:28:22 +0000 Subject: [PATCH 2/2] Add project delete endpoint --- agent-tasks/project-deletion.md | 55 +++++++++++++-------------------- backend/bootstrap/main.go | 1 + backend/controllers/projects.go | 42 +++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 33 deletions(-) diff --git a/agent-tasks/project-deletion.md b/agent-tasks/project-deletion.md index 54845d711..c1b561fdf 100644 --- a/agent-tasks/project-deletion.md +++ b/agent-tasks/project-deletion.md @@ -1,15 +1,12 @@ -# Project Deletion: Implementation Plan (Essentials) +# Project Deletion: Backend-Only Plan ## Scope -- Add a Delete Project action on the Project Details screen. -- Add backend support to soft-delete a project. +- Add backend support to soft-delete a project via a new API endpoint. +- UI work is out of scope (lives in a separate repo). ## Current State -- UI files: `projects.$projectid.tsx`, `projects.index.tsx`. -- UI API: `UI/src/api/orchestrator_projects.ts` has get/update; no delete. -- ServerFns: `UI/src/api/orchestrator_serverFunctions.ts` has `getProjectFn`, `getProjectsFn`, `updateProjectFn`. -- Backend routes: `backend/bootstrap/main.go` exposes GET, PUT for projects; no DELETE. -- Model: `Project` uses `gorm.Model` (soft-delete supported). +- Backend exposes GET, PUT for projects; no DELETE yet (`backend/bootstrap/main.go`). +- Model `Project` uses `gorm.Model` (has `DeletedAt`) so soft-delete is supported by default. ## API - Method: DELETE @@ -18,41 +15,33 @@ - Responses: 204 on success; 404 not found; 403 forbidden; 500 on error. ## Backend Steps -1) Add route in `backend/bootstrap/main.go`: - - `projectsApiGroup.DELETE("/:project_id/", controllers.DeleteProjectApi)` -2) Implement `DeleteProjectApi` in `backend/controllers/projects.go`: - - Resolve org from headers; fetch project by `org.ID` and `project_id`. - - `models.DB.GormDB.Delete(&project)` to soft-delete. - - Return 204. -3) Migrations: none (soft-delete already present). - -## UI Steps -1) Add `deleteProject` to `UI/src/api/orchestrator_projects.ts` (DELETE request with existing headers). Return empty object on 204. -2) Add `deleteProjectFn` to `UI/src/api/orchestrator_serverFunctions.ts` wrapping `deleteProject`. -3) Update `projects.$projectid.tsx`: - - Import `useRouter`, `useToast`, `deleteProjectFn`, and `AlertDialog` components; add a destructive Delete button. - - On confirm: call `deleteProjectFn({ data: { projectId, organisationId, userId } })`, toast success, navigate to `/dashboard/projects`. - - On error: toast destructive. +1) Route: In `backend/bootstrap/main.go`, register `projectsApiGroup.DELETE("/:project_id/", controllers.DeleteProjectApi)` inside the `if enableApi` block. +2) Controller: In `backend/controllers/projects.go`, add `DeleteProjectApi`: + - Resolve org from headers (`ORGANISATION_ID_KEY`, `ORGANISATION_SOURCE_KEY`). + - Load org by `external_id` + `external_source`. + - Load project by `projects.organisation_id = org.ID AND projects.id = :project_id`. + - Soft delete via `models.DB.GormDB.Delete(&project)` and return `204`. +3) Migrations: none (soft-delete already present and indexed). + +## Out of Scope +- Any UI wiring or server functions in the UI repo. ## Security -- Endpoint protected by existing API middleware; UI invokes via server function to keep secrets server-side. +- Endpoint is protected by existing API middleware (`InternalApiAuth`, `HeadersApiAuth`). ## Acceptance Criteria -- Delete button shows on Project Details; confirmation dialog appears. -- Confirm deletes the project (soft-delete), redirects to Projects list, and shows success toast. -- Deleted project no longer appears in list; details by ID returns 404. +- DELETE `/api/projects/:project_id/` returns 204 on success. +- After deletion, `GET /api/projects/` no longer lists the project (GORM soft-delete filtering applies). +- `GET /api/projects/:project_id/` returns 404 for the deleted project. ## Test Plan -- API: DELETE with valid headers → 204; repeat → 404; missing headers → 403. -- UI: Confirm delete redirects to list; failure shows error toast. +- API: DELETE with valid headers → 204; repeat → 404; missing/invalid headers → 403. +- API: Verify project disappears from list endpoint; details endpoint returns 404 post-delete. ## Estimate -- Backend: 1–2 hours; UI: ~1 hour; total: ~2–3 hours. +- Backend: 1–2 hours. ## Pointers - Routes: `backend/bootstrap/main.go` - Controller: `backend/controllers/projects.go` - Model: `backend/models/orgs.go` -- UI API: `UI/src/api/orchestrator_projects.ts` -- ServerFn: `UI/src/api/orchestrator_serverFunctions.ts` -- Screen: `UI/src/routes/_authenticated/_dashboard/dashboard/projects.$projectid.tsx` diff --git a/backend/bootstrap/main.go b/backend/bootstrap/main.go index 99ce5c9b3..9f336a4c5 100644 --- a/backend/bootstrap/main.go +++ b/backend/bootstrap/main.go @@ -239,6 +239,7 @@ func Bootstrap(templates embed.FS, diggerController controllers.DiggerController projectsApiGroup.GET("/", controllers.ListProjectsApi) projectsApiGroup.GET("/:project_id/", controllers.ProjectsDetailsApi) projectsApiGroup.PUT("/:project_id/", controllers.UpdateProjectApi) + projectsApiGroup.DELETE("/:project_id/", controllers.DeleteProjectApi) githubApiGroup := apiGroup.Group("/github") githubApiGroup.POST("/link", controllers.LinkGithubInstallationToOrgApi) diff --git a/backend/controllers/projects.go b/backend/controllers/projects.go index 87cdf0162..fd2686bc4 100644 --- a/backend/controllers/projects.go +++ b/backend/controllers/projects.go @@ -160,6 +160,48 @@ func UpdateProjectApi(c *gin.Context) { c.JSON(http.StatusOK, project) } +func DeleteProjectApi(c *gin.Context) { + // assume all exists as validated in middleware + organisationId := c.GetString(middleware.ORGANISATION_ID_KEY) + organisationSource := c.GetString(middleware.ORGANISATION_SOURCE_KEY) + projectId := c.Param("project_id") + + var org models.Organisation + err := models.DB.GormDB.Where("external_id = ? AND external_source = ?", organisationId, organisationSource).First(&org).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + slog.Info("Organisation not found", "organisationId", organisationId, "source", organisationSource) + c.String(http.StatusNotFound, "Could not find organisation: "+organisationId) + } else { + slog.Error("Error fetching organisation", "organisationId", organisationId, "source", organisationSource, "error", err) + c.String(http.StatusInternalServerError, "Error fetching organisation") + } + return + } + + var project models.Project + err = models.DB.GormDB.Where("projects.organisation_id = ? AND projects.id = ?", org.ID, projectId).First(&project).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + slog.Info("Project not found", "organisationId", organisationId, "orgId", org.ID) + c.String(http.StatusNotFound, "Could not find project") + } else { + slog.Error("Error fetching project", "organisationId", organisationId, "orgId", org.ID, "error", err) + c.String(http.StatusInternalServerError, "Unknown error occurred while fetching database") + } + return + } + + err = models.DB.GormDB.Delete(&project).Error + if err != nil { + slog.Error("Error deleting project", "organisationId", organisationId, "orgId", org.ID, "projectId", projectId, "error", err) + c.String(http.StatusInternalServerError, "Unknown error occurred while deleting project") + return + } + + c.Status(http.StatusNoContent) +} + func FindProjectsForRepo(c *gin.Context) { repo := c.Param("repo") orgId, exists := c.Get(middleware.ORGANISATION_ID_KEY)