# Story 1.3: Diagram Access Control & Management Status: done ## Story As a diagram owner, I want to rename, delete, and control access to my diagrams, so that I can manage my workspace and protect my work. ## Acceptance Criteria 1. **Given** I am viewing a diagram I own, **When** I click the diagram title in the dashboard card or editor header, **Then** it becomes an inline editable field, **And** changes are saved on blur or Enter keypress via PATCH /diagrams/:id. 2. **Given** I own a diagram, **When** I click the delete action on a diagram card, **Then** a confirmation dialog appears warning that deletion is permanent, **And** on confirmation the diagram is soft-deleted (sets `deletedAt` timestamp) and removed from my dashboard. 3. **Given** I am not the owner of a diagram, **When** I attempt to access the diagram via direct URL, **Then** I receive a 403 Forbidden response, **And** I see a "You don't have access to this diagram" message. 4. **Given** any API request to diagram endpoints (GET /:id, PATCH /:id, DELETE /:id), **When** the request is processed, **Then** server-side middleware verifies the authenticated user is the diagram owner (or has a valid share token — covered in Epic 6), **And** unauthorized requests return 403 before any data mutation. ## Tasks / Subtasks - [x] Task 1: Add PATCH and DELETE endpoints to diagram router (AC: #1, #2, #4) - [x] 1.1: Add `updateDiagramSchema` Zod schema for PATCH (title only for now) - [x] 1.2: Add PATCH /:id endpoint — ownership check → update title → return updated diagram - [x] 1.3: Add DELETE /:id endpoint — ownership check → soft-delete via `deletedAt = new Date()` → return success - [x] 1.4: Refactor GET /:id to distinguish 404 (not found) from 403 (not owner): query without userId filter first, then check ownership - [x] Task 2: Add inline rename to DiagramCard (AC: #1) - [x] 2.1: Add editable title state to DiagramCard — click title → input field, save on blur/Enter, cancel on Escape - [x] 2.2: PATCH mutation via Hono RPC client with React Query invalidation - [x] 2.3: Add kebab menu (DropdownMenu) to DiagramCard with Rename and Delete actions - [x] Task 3: Add delete confirmation dialog (AC: #2) - [x] 3.1: AlertDialog with warning text: "This action cannot be undone. The diagram will be permanently deleted." - [x] 3.2: DELETE mutation via Hono RPC client with React Query invalidation + toast confirmation - [x] Task 4: Add inline rename to diagram editor header (AC: #1) - [x] 4.1: Replace static title in editor page with editable field — same blur/Enter/Escape pattern - [x] 4.2: PATCH mutation with React Query invalidation - [x] Task 5: Handle 403 error state in diagram editor (AC: #3) - [x] 5.1: Detect 403 response from GET /:id and show "You don't have access to this diagram" with appropriate icon - [x] 5.2: Differentiate 403 (access denied) from 404 (not found) in error UI - [x] Task 6: Tests (AC: all) - [x] 6.1: API schema validation tests for updateDiagramSchema - [x] 6.2: Ownership check logic tests (403 vs 404 distinction) ## Dev Notes ### API — PATCH and DELETE Endpoints Modify `packages/api/src/modules/diagram/router.ts` to add two new endpoints: **PATCH /:id — Rename diagram:** ```typescript const updateDiagramBodySchema = z.object({ title: z.string().min(1).max(255).optional(), }).refine((data) => Object.keys(data).length > 0, { message: "At least one field must be provided", }); .patch("/:id", enforceAuth, validate("json", updateDiagramBodySchema), async (c) => { const [d] = await db.select().from(diagram) .where(and(eq(diagram.id, c.req.param("id")), isNull(diagram.deletedAt))); if (!d) { throw new HttpException(HttpStatusCode.NOT_FOUND, { code: "error.notFound" }); } if (d.userId !== c.var.user.id) { throw new HttpException(HttpStatusCode.FORBIDDEN, { code: "error.forbidden", message: "You don't have access to this diagram" }); } const [updated] = await db.update(diagram) .set(c.req.valid("json")) .where(eq(diagram.id, c.req.param("id"))) .returning(); return c.json({ data: updated }); }) ``` **DELETE /:id — Soft-delete diagram:** ```typescript .delete("/:id", enforceAuth, async (c) => { const [d] = await db.select().from(diagram) .where(and(eq(diagram.id, c.req.param("id")), isNull(diagram.deletedAt))); if (!d) { throw new HttpException(HttpStatusCode.NOT_FOUND, { code: "error.notFound" }); } if (d.userId !== c.var.user.id) { throw new HttpException(HttpStatusCode.FORBIDDEN, { code: "error.forbidden", message: "You don't have access to this diagram" }); } await db.update(diagram) .set({ deletedAt: new Date() }) .where(eq(diagram.id, c.req.param("id"))); return c.json({ data: { success: true } }); }) ``` **Refactor GET /:id — 403 vs 404 distinction:** Current GET /:id combines ownership + existence in one WHERE clause, returning 404 for both cases. Refactor to: ```typescript .get("/:id", enforceAuth, async (c) => { const [d] = await db.select().from(diagram) .where(and(eq(diagram.id, c.req.param("id")), isNull(diagram.deletedAt))); if (!d) { throw new HttpException(HttpStatusCode.NOT_FOUND, { code: "error.notFound" }); } if (d.userId !== c.var.user.id) { throw new HttpException(HttpStatusCode.FORBIDDEN, { code: "error.forbidden", message: "You don't have access to this diagram" }); } return c.json({ data: d }); }) ``` Note: When share tokens are added (Epic 6), the ownership check will expand to also accept valid share tokens: `if (d.userId !== c.var.user.id && !validShareToken) { 403 }`. ### Frontend — DiagramCard Enhancements Modify `apps/web/src/modules/diagram/components/DiagramCard.tsx`: **Add kebab menu with Rename/Delete actions:** - Use `DropdownMenu` (not ContextMenu) — consistent with Story 1.2's ProjectContextMenu pattern - Menu items: Rename (opens inline edit), Delete (opens AlertDialog) - Import: `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuItem` from `@turbostarter/ui-web/dropdown-menu` - Import: `AlertDialog`, `AlertDialogAction`, `AlertDialogCancel`, `AlertDialogContent`, `AlertDialogDescription`, `AlertDialogFooter`, `AlertDialogHeader`, `AlertDialogTitle` from `@turbostarter/ui-web/alert-dialog` **Inline editable title:** - State: `isEditing` boolean, `editValue` string - Click title (or Rename from menu) → show `Input` component - Save on blur/Enter → `api.diagrams[":id"].$patch({ param: { id }, json: { title: editValue } })` - Cancel on Escape → revert to original title - React Query invalidation: `["diagrams"]` and `["diagram", id]` - Use `toast()` from `sonner` for success/error feedback **Delete confirmation:** - AlertDialog matches Story 1.2's project delete pattern - On confirm → `api.diagrams[":id"].$delete({ param: { id } })` - React Query invalidation: `["diagrams"]` + navigate away if in editor - Toast: "Diagram deleted" ### Frontend — Editor Header Rename Modify `apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx`: Replace the static `