mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
fix: Reject collections.update requests that include both description and data (#12648)
This commit is contained in:
@@ -1257,6 +1257,23 @@ describe("#collections.create", () => {
|
|||||||
expect(res.status).toEqual(400);
|
expect(res.status).toEqual(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects providing both description and data", async () => {
|
||||||
|
const user = await buildUser();
|
||||||
|
const res = await server.post("/api/collections.create", user, {
|
||||||
|
body: {
|
||||||
|
name: "Test",
|
||||||
|
description: "Test",
|
||||||
|
data: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{ type: "paragraph", content: [{ type: "text", text: "Test" }] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
it("should allow setting sharing to false", async () => {
|
it("should allow setting sharing to false", async () => {
|
||||||
const user = await buildUser();
|
const user = await buildUser();
|
||||||
const res = await server.post("/api/collections.create", user, {
|
const res = await server.post("/api/collections.create", user, {
|
||||||
@@ -1448,6 +1465,50 @@ describe("#collections.update", () => {
|
|||||||
expect(collection.content).toBeTruthy();
|
expect(collection.content).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("replaces rendered content when description is updated post-create", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const admin = await buildAdmin({ teamId: team.id });
|
||||||
|
|
||||||
|
const createRes = await server.post("/api/collections.create", admin, {
|
||||||
|
headers: { "x-api-version": "3" },
|
||||||
|
body: { name: "Foo", description: "Original" },
|
||||||
|
});
|
||||||
|
const { id } = (await createRes.json()).data;
|
||||||
|
|
||||||
|
const updateRes = await server.post("/api/collections.update", admin, {
|
||||||
|
headers: { "x-api-version": "3" },
|
||||||
|
body: { id, description: "Replaced" },
|
||||||
|
});
|
||||||
|
expect(updateRes.status).toEqual(200);
|
||||||
|
|
||||||
|
const infoRes = await server.post("/api/collections.info", admin, {
|
||||||
|
headers: { "x-api-version": "3" },
|
||||||
|
body: { id },
|
||||||
|
});
|
||||||
|
const content = JSON.stringify((await infoRes.json()).data.data);
|
||||||
|
expect(content).toContain("Replaced");
|
||||||
|
expect(content).not.toContain("Original");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects providing both description and data", async () => {
|
||||||
|
const team = await buildTeam();
|
||||||
|
const admin = await buildAdmin({ teamId: team.id });
|
||||||
|
const collection = await buildCollection({ teamId: team.id });
|
||||||
|
const res = await server.post("/api/collections.update", admin, {
|
||||||
|
body: {
|
||||||
|
id: collection.id,
|
||||||
|
description: "Test",
|
||||||
|
data: {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{ type: "paragraph", content: [{ type: "text", text: "Test" }] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
it("allows editing data", async () => {
|
it("allows editing data", async () => {
|
||||||
const team = await buildTeam();
|
const team = await buildTeam();
|
||||||
const admin = await buildAdmin({ teamId: team.id });
|
const admin = await buildAdmin({ teamId: team.id });
|
||||||
|
|||||||
@@ -15,39 +15,50 @@ const BaseIdSchema = z.object({
|
|||||||
id: zodIdType(),
|
id: zodIdType(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** The landing page can be set from description (markdown) or data (rich content), but not both. */
|
||||||
|
const refineBodyContent = <T extends { description?: unknown; data?: unknown }>(
|
||||||
|
body: T
|
||||||
|
) => isUndefined(body.description) || isUndefined(body.data);
|
||||||
|
|
||||||
|
const bodyContentError = {
|
||||||
|
error: "Only one of description or data may be provided",
|
||||||
|
};
|
||||||
|
|
||||||
export const CollectionsCreateSchema = BaseSchema.extend({
|
export const CollectionsCreateSchema = BaseSchema.extend({
|
||||||
body: z.object({
|
body: z
|
||||||
name: z.string(),
|
.object({
|
||||||
color: z
|
name: z.string(),
|
||||||
.string()
|
color: z
|
||||||
.regex(ValidateColor.regex, { message: ValidateColor.message })
|
.string()
|
||||||
.nullish(),
|
.regex(ValidateColor.regex, { message: ValidateColor.message })
|
||||||
description: z.string().nullish(),
|
.nullish(),
|
||||||
data: ProsemirrorSchema({ allowEmpty: true }).nullish(),
|
description: z.string().nullish(),
|
||||||
permission: z
|
data: ProsemirrorSchema({ allowEmpty: true }).nullish(),
|
||||||
.enum(CollectionPermission)
|
permission: z
|
||||||
.nullish()
|
.enum(CollectionPermission)
|
||||||
.transform((val) => (isUndefined(val) ? null : val)),
|
.nullish()
|
||||||
sharing: z.boolean().prefault(true),
|
.transform((val) => (isUndefined(val) ? null : val)),
|
||||||
icon: zodIconType().optional(),
|
sharing: z.boolean().prefault(true),
|
||||||
sort: z
|
icon: zodIconType().optional(),
|
||||||
.object({
|
sort: z
|
||||||
field: z.union([z.literal("title"), z.literal("index")]),
|
.object({
|
||||||
direction: z.union([z.literal("asc"), z.literal("desc")]),
|
field: z.union([z.literal("title"), z.literal("index")]),
|
||||||
})
|
direction: z.union([z.literal("asc"), z.literal("desc")]),
|
||||||
.prefault(Collection.DEFAULT_SORT),
|
})
|
||||||
index: z
|
.prefault(Collection.DEFAULT_SORT),
|
||||||
.string()
|
index: z
|
||||||
.regex(ValidateIndex.regex, { message: ValidateIndex.message })
|
.string()
|
||||||
.max(ValidateIndex.maxLength, {
|
.regex(ValidateIndex.regex, { message: ValidateIndex.message })
|
||||||
message: `Must be ${ValidateIndex.maxLength} or fewer characters long`,
|
.max(ValidateIndex.maxLength, {
|
||||||
})
|
message: `Must be ${ValidateIndex.maxLength} or fewer characters long`,
|
||||||
.optional(),
|
})
|
||||||
commenting: z.boolean().nullish(),
|
.optional(),
|
||||||
templateManagement: z
|
commenting: z.boolean().nullish(),
|
||||||
.enum([CollectionPermission.Admin, CollectionPermission.ReadWrite])
|
templateManagement: z
|
||||||
.prefault(CollectionPermission.Admin),
|
.enum([CollectionPermission.Admin, CollectionPermission.ReadWrite])
|
||||||
}),
|
.prefault(CollectionPermission.Admin),
|
||||||
|
})
|
||||||
|
.refine(refineBodyContent, bodyContentError),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CollectionsCreateReq = z.infer<typeof CollectionsCreateSchema>;
|
export type CollectionsCreateReq = z.infer<typeof CollectionsCreateSchema>;
|
||||||
@@ -188,7 +199,7 @@ export const CollectionsUpdateSchema = BaseSchema.extend({
|
|||||||
templateManagement: z
|
templateManagement: z
|
||||||
.enum([CollectionPermission.Admin, CollectionPermission.ReadWrite])
|
.enum([CollectionPermission.Admin, CollectionPermission.ReadWrite])
|
||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}).refine(refineBodyContent, bodyContentError),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CollectionsUpdateReq = z.infer<typeof CollectionsUpdateSchema>;
|
export type CollectionsUpdateReq = z.infer<typeof CollectionsUpdateSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user