Add PATCH and DELETE endpoints with ownership checks (403 vs 404), inline rename on DiagramCard and editor header, delete confirmation dialog, and differentiated error states for forbidden/not-found. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
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
-
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.
-
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
deletedAttimestamp) and removed from my dashboard. -
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.
-
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
- Task 1: Add PATCH and DELETE endpoints to diagram router (AC: #1, #2, #4)
- 1.1: Add
updateDiagramSchemaZod schema for PATCH (title only for now) - 1.2: Add PATCH /:id endpoint — ownership check → update title → return updated diagram
- 1.3: Add DELETE /:id endpoint — ownership check → soft-delete via
deletedAt = new Date()→ return success - 1.4: Refactor GET /:id to distinguish 404 (not found) from 403 (not owner): query without userId filter first, then check ownership
- 1.1: Add
- Task 2: Add inline rename to DiagramCard (AC: #1)
- 2.1: Add editable title state to DiagramCard — click title → input field, save on blur/Enter, cancel on Escape
- 2.2: PATCH mutation via Hono RPC client with React Query invalidation
- 2.3: Add kebab menu (DropdownMenu) to DiagramCard with Rename and Delete actions
- Task 3: Add delete confirmation dialog (AC: #2)
- 3.1: AlertDialog with warning text: "This action cannot be undone. The diagram will be permanently deleted."
- 3.2: DELETE mutation via Hono RPC client with React Query invalidation + toast confirmation
- Task 4: Add inline rename to diagram editor header (AC: #1)
- 4.1: Replace static title in editor page with editable field — same blur/Enter/Escape pattern
- 4.2: PATCH mutation with React Query invalidation
- Task 5: Handle 403 error state in diagram editor (AC: #3)
- 5.1: Detect 403 response from GET /:id and show "You don't have access to this diagram" with appropriate icon
- 5.2: Differentiate 403 (access denied) from 404 (not found) in error UI
- Task 6: Tests (AC: all)
- 6.1: API schema validation tests for updateDiagramSchema
- 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:
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:
.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:
.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,DropdownMenuItemfrom@turbostarter/ui-web/dropdown-menu - Import:
AlertDialog,AlertDialogAction,AlertDialogCancel,AlertDialogContent,AlertDialogDescription,AlertDialogFooter,AlertDialogHeader,AlertDialogTitlefrom@turbostarter/ui-web/alert-dialog
Inline editable title:
- State:
isEditingboolean,editValuestring - Click title (or Rename from menu) → show
Inputcomponent - 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()fromsonnerfor 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 <h1>{data?.data?.title}</h1> with an inline editable title using the same pattern as DiagramCard. The editor page already fetches the diagram — just add edit state and PATCH mutation.
Frontend — 403 Error Handling
The editor page currently shows a generic error for any API failure. Enhance to distinguish:
const { data, isLoading, error } = useQuery({
queryKey: ["diagram", params.id],
queryFn: async () => {
const res = await api.diagrams[":id"].$get({ param: { id: params.id } });
if (res.status === 403) {
throw new Error("forbidden");
}
if (!res.ok) {
throw new Error("not-found");
}
return await res.json();
},
});
Render different error UIs:
- 403:
Icons.ShieldAlert+ "You don't have access to this diagram" - 404/other:
Icons.AlertTriangle+ "Failed to load diagram. It may have been deleted."
Project Structure Notes
packages/api/src/modules/diagram/router.ts— MODIFIED: add PATCH /:id, DELETE /:id, refactor GET /:idapps/web/src/modules/diagram/components/DiagramCard.tsx— MODIFIED: add kebab menu, inline rename, delete dialogapps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx— MODIFIED: add editable title, 403 handlingpackages/api/tests/diagram/diagram-access-control.test.ts— NEW: ownership and access control schema tests- Alignment: All paths match architecture doc patterns. No new modules needed.
Anti-Patterns to Avoid
- NEVER hard-delete diagrams — always soft-delete via
deletedAt = new Date(). ThedeletedAtcolumn already exists. - NEVER return 404 for ownership failures — return 403 to distinguish "doesn't exist" from "not authorized". This is a security-aware choice for this story (the AC explicitly requires 403).
- NEVER use raw fetch — always use Hono RPC client (
api.diagrams[":id"].$patch(...)) - NEVER inline
.parse()in Hono handlers — usevalidate("json", schema)middleware - NEVER put business logic in API routers — ownership check is a simple comparison, acceptable inline. If it grows, extract to a helper.
- DO NOT change the diagram DB schema —
deletedAtcolumn already exists from Story 1.1 - DO NOT implement share token access control — that's Epic 6. Leave a comment placeholder in the ownership check.
- DO NOT add drag-and-drop — that's Story 1.4
- DO NOT add
Icons.ShieldAlertto icons.tsx without checking if it already exists — check first
Previous Story Intelligence (Story 1.2)
Key learnings to carry forward:
DropdownMenupreferred overContextMenufor action menus (more discoverable, accessible)db.transaction()for multi-step operations — NOT needed here since each endpoint does a single mutationtimeAgo()exported from DiagramCard for reuse- Hono RPC client pattern:
api.diagrams[":id"].$get({ param: { id } })— NOT raw fetch toast()fromsonnerfor user feedback- React Query invalidation:
queryClient.invalidateQueries({ queryKey: ["diagrams"] }) - PATCH validation: use
.refine()to require at least one field (lesson from code review issue #4) - 107 existing tests pass — don't break them
Git Intelligence
Recent commits:
85e06c2 feat: implement Story 1.2 — organize diagrams into projects— 12 files, project CRUD + sidebar392da38 feat: implement Story 1.1 — create and view diagrams— 20 files, diagram schema + CRUD + dashboard
Established patterns:
- Commit message:
feat: implement Story X.Y — description - Co-located tests:
packages/api/tests/diagram/ - Zod schemas exported from router files for type-safe client usage
References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 1.3] — Full AC and technical notes
- [Source: _bmad-output/planning-artifacts/architecture.md#Implementation Patterns] — Naming, structure, enforcement rules
- [Source: _bmad-output/planning-artifacts/architecture.md#Enforcement Guidelines] — 7 mandatory rules for AI agents
- [Source: _bmad-output/implementation-artifacts/1-2-organize-diagrams-into-projects.md] — Previous story learnings
- [Source: _bmad-output/project-context.md] — 62 critical implementation rules
- [Source: packages/api/src/modules/diagram/router.ts] — Current diagram router (GET /, GET /:id, POST /)
- [Source: packages/db/src/schema/diagram.ts] — Diagram schema with deletedAt column
- [Source: apps/web/src/modules/diagram/components/DiagramCard.tsx] — Current card component (no menu/edit)
- [Source: apps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx] — Current editor page (static title)
Dev Agent Record
Agent Model Used
Claude Opus 4.6
Debug Log References
- TypeScript: 0 errors (both
packages/apiandapps/web) - Tests: 120 passed (107 existing + 13 new) — 0 failures
Completion Notes List
- Task 1: Added
updateDiagramBodySchemawith.refine()for at least one field. PATCH /:id and DELETE /:id endpoints with ownership check (403 vs 404 distinction). Refactored GET /:id to separate existence from ownership. - Task 2: DiagramCard converted to client component with inline editable title, PATCH mutation via Hono RPC, DropdownMenu (kebab) with Rename/Delete actions.
- Task 3: AlertDialog for delete confirmation with destructive styling. DELETE mutation via Hono RPC with React Query invalidation + toast.
- Task 4: Editor page title replaced with inline editable field using same blur/Enter/Escape pattern as DiagramCard.
- Task 5: 403 detection via
res.status === 403in useQuery. Differentiated error UIs:Icons.Lockfor 403,Icons.AlertTrianglefor 404. Note: UsedIcons.Lockinstead ofIcons.ShieldAlert(not available in icons.tsx). - Task 6: 13 tests total — 7 schema validation tests for
updateDiagramBodySchema+ 6 ownership check logic tests (403 vs 404 contract, shared helper, Epic 6 placeholder). - Code Review Fixes (Claude Opus 4.6):
- H1: Added onClick to DiagramCard title for inline edit (AC #1 compliance)
- H2: Added 6 ownership check logic tests (Task 6.2)
- M1+L2: Replaced useState error anti-pattern with typed DiagramError class + useQuery error property
- M2: Added custom retry function to skip retries for 403/404 DiagramErrors
- M3: Extracted
getOwnedDiagram()helper to DRY ownership check across GET/PATCH/DELETE - L1: Added useEffect to sync editValue when diagram.title changes externally
File List
packages/api/src/modules/diagram/router.ts— MODIFIED: AddedupdateDiagramBodySchema, PATCH /:id, DELETE /:id, refactored GET /:id for 403 vs 404apps/web/src/modules/diagram/components/DiagramCard.tsx— MODIFIED: Added inline rename, kebab menu (DropdownMenu), delete confirmation (AlertDialog), PATCH/DELETE mutationsapps/web/src/app/[locale]/dashboard/(user)/diagram/[id]/page.tsx— MODIFIED: Added inline editable title, 403/404 error differentiationpackages/api/tests/diagram/diagram-access-control.test.ts— NEW: 7 schema validation tests for updateDiagramBodySchema