diff --git a/apps/web/e2e/auth-projects.spec.ts b/apps/web/e2e/auth-projects.spec.ts index 456c1397..4d4b2b3d 100644 --- a/apps/web/e2e/auth-projects.spec.ts +++ b/apps/web/e2e/auth-projects.spec.ts @@ -86,8 +86,137 @@ test("login loads backend projects list on /projects", async ({ context, page }) await page.getByRole("button", { name: "Sign in" }).click(); await expect(page).toHaveURL(/\/projects/); - await page.waitForResponse((response) => - response.url().includes("/api/editor/projects"), - ); await expect(page.getByText("Backend Project Alpha")).toBeVisible(); }); + +test("deleting project from /projects sends cloud DELETE request", async ({ + context, + page, +}) => { + await page.addInitScript(() => { + window.localStorage.setItem("vibecut_workspace_id", "ws-1"); + }); + + await context.addCookies([ + { + name: "refresh_token", + value: "e2e-refresh-token", + domain: "127.0.0.1", + path: "/", + httpOnly: false, + secure: false, + sameSite: "Lax", + }, + ]); + + await page.route("**/api/auth/user/refresh", async (route) => { + await route.fulfill({ + status: 401, + contentType: "application/json", + body: JSON.stringify({ + status: "error", + message: "Invalid refresh token", + statusCode: 401, + }), + }); + }); + + await page.route("**/api/auth/user/login", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + token: "backend-jwt-token", + user: { + id: "u-1", + username: "john", + email: "john@example.com", + }, + }), + }); + }); + + const deleteRequestPromise = page.waitForRequest( + (request) => + request.method() === "DELETE" && + request.url().includes("/api/editor/projects/proj-remote-1"), + ); + + await page.route("**/api/editor/projects/*", async (route) => { + const workspaceHeader = route.request().headers()["x-workspace-id"]; + if (!workspaceHeader) { + await route.fulfill({ + status: 400, + contentType: "application/json", + body: JSON.stringify({ + message: "x-workspace-id header is required", + error: "Bad Request", + statusCode: 400, + }), + }); + return; + } + + if (route.request().method() === "DELETE") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ ok: true }), + }); + return; + } + + await route.fulfill({ status: 405 }); + }); + + await page.route("**/api/editor/projects**", async (route) => { + const workspaceHeader = route.request().headers()["x-workspace-id"]; + if (!workspaceHeader) { + await route.fulfill({ + status: 400, + contentType: "application/json", + body: JSON.stringify({ + message: "x-workspace-id header is required", + error: "Bad Request", + statusCode: 400, + }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + items: [ + { + id: "proj-remote-1", + workspaceId: "ws-1", + ownerId: 1, + name: "Backend Project Alpha", + version: 4, + updatedAt: "2026-02-09T12:00:00.000Z", + createdAt: "2026-02-08T12:00:00.000Z", + }, + ], + total: 1, + }), + }); + }); + + await page.goto("/auth/login"); + + await page.getByLabel("Username").fill("john"); + await page.getByLabel("Password").fill("12345678"); + await page.getByRole("button", { name: "Sign in" }).click(); + + await expect(page).toHaveURL(/\/projects/); + await expect(page.getByText("Backend Project Alpha")).toBeVisible(); + + await page.getByRole("button", { name: "Project menu" }).first().click(); + await page.getByRole("menuitem", { name: "Delete" }).click(); + await page.getByPlaceholder("DELETE").fill("DELETE"); + await page.getByRole("button", { name: "Delete project" }).click(); + + await deleteRequestPromise; +}); diff --git a/apps/web/src/core/managers/project-manager.ts b/apps/web/src/core/managers/project-manager.ts index 7e43984d..16d7cbc1 100644 --- a/apps/web/src/core/managers/project-manager.ts +++ b/apps/web/src/core/managers/project-manager.ts @@ -307,8 +307,33 @@ export class ProjectManager { if (uniqueIds.length === 0) return; try { + const token = authSession.getToken(); + const deletedProjectIds = + token === null + ? uniqueIds + : ( + await Promise.all( + uniqueIds.map(async (id) => { + try { + await editorCloudApi.deleteProject({ projectId: id, token }); + return id; + } catch (error) { + console.error(`Failed to delete cloud project ${id}:`, error); + return null; + } + }), + ) + ).filter((id): id is string => id !== null); + + if (deletedProjectIds.length === 0) { + toast.error("Failed to delete projects", { + description: "Cloud delete failed. Please try again.", + }); + return; + } + await Promise.all( - uniqueIds.map((id) => + deletedProjectIds.map((id) => Promise.all([ storageService.deleteProjectMedia({ projectId: id }), storageService.deleteProject({ id }), @@ -316,10 +341,13 @@ export class ProjectManager { ), ); - const idSet = new Set(uniqueIds); + const idSet = new Set(deletedProjectIds); this.savedProjects = this.savedProjects.filter( (project) => !idSet.has(project.id), ); + deletedProjectIds.forEach((id) => { + this.remoteVersionByProjectId.delete(id); + }); const shouldClearActive = this.active && idSet.has(this.active.metadata.id); @@ -331,8 +359,18 @@ export class ProjectManager { } this.notify(); + + if (deletedProjectIds.length < uniqueIds.length) { + toast.error("Some projects were not deleted", { + description: + "Cloud sync failed for some selected projects. Please try again.", + }); + } } catch (error) { console.error("Failed to delete projects:", error); + toast.error("Failed to delete projects", { + description: error instanceof Error ? error.message : "Please try again", + }); } } diff --git a/apps/web/src/lib/cloud-sync/editor-api.ts b/apps/web/src/lib/cloud-sync/editor-api.ts index 2ba46a84..72233d34 100644 --- a/apps/web/src/lib/cloud-sync/editor-api.ts +++ b/apps/web/src/lib/cloud-sync/editor-api.ts @@ -133,7 +133,7 @@ const requestWithAuthRetry = async ({ body, }: { path: string; - method: "GET" | "PUT" | "POST"; + method: "GET" | "PUT" | "POST" | "DELETE"; token: string; body?: unknown; }): Promise => { @@ -277,6 +277,18 @@ export const editorCloudApi = { token, body: payload, }), + deleteProject: ({ + projectId, + token, + }: { + projectId: string; + token: string; + }) => + requestWithAuthRetry({ + path: `/editor/projects/${projectId}`, + method: "DELETE", + token, + }), createFileUpload: ({ token, payload,