feat: implement Story 1.3 — diagram access control and management

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>
This commit is contained in:
Alejandro Gutiérrez
2026-02-23 22:18:28 +00:00
parent 85e06c25c7
commit e9cd685d3d
6 changed files with 790 additions and 53 deletions

View File

@@ -27,6 +27,46 @@ export const createDiagramSchema = z.object({
projectId: z.string().optional(),
});
export const updateDiagramBodySchema = z
.object({
title: z.string().min(1).max(255).optional(),
})
.refine((data) => data.title !== undefined, {
message: "At least one field must be provided",
});
/**
* Fetch a diagram by ID and verify ownership.
* Returns 404 if not found, 403 if not owned by userId.
* Future: also accept valid share tokens (Epic 6).
*/
async function getOwnedDiagram(diagramId: string, userId: string) {
const [d] = await db
.select()
.from(diagram)
.where(
and(
eq(diagram.id, diagramId),
isNull(diagram.deletedAt),
),
);
if (!d) {
throw new HttpException(HttpStatusCode.NOT_FOUND, {
code: "error.notFound",
});
}
if (d.userId !== userId) {
throw new HttpException(HttpStatusCode.FORBIDDEN, {
code: "error.forbidden",
message: "You don't have access to this diagram",
});
}
return d;
}
export const diagramRouter = new Hono()
.get("/", enforceAuth, validate("query", listDiagramsQuerySchema), async (c) => {
const { projectId, unorganized } = c.req.valid("query");
@@ -51,23 +91,7 @@ export const diagramRouter = new Hono()
return c.json({ data: diagrams });
})
.get("/:id", enforceAuth, async (c) => {
const [d] = await db
.select()
.from(diagram)
.where(
and(
eq(diagram.id, c.req.param("id")),
eq(diagram.userId, c.var.user.id),
isNull(diagram.deletedAt),
),
);
if (!d) {
throw new HttpException(HttpStatusCode.NOT_FOUND, {
code: "error.notFound",
});
}
const d = await getOwnedDiagram(c.req.param("id"), c.var.user.id);
return c.json({ data: d });
})
.post(
@@ -86,4 +110,30 @@ export const diagramRouter = new Hono()
return c.json({ data: created });
},
);
)
.patch(
"/:id",
enforceAuth,
validate("json", updateDiagramBodySchema),
async (c) => {
await getOwnedDiagram(c.req.param("id"), c.var.user.id);
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", enforceAuth, async (c) => {
await getOwnedDiagram(c.req.param("id"), c.var.user.id);
await db
.update(diagram)
.set({ deletedAt: new Date() })
.where(eq(diagram.id, c.req.param("id")));
return c.json({ data: { success: true } });
});