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:
@@ -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 } });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user