feat: sanitize input for checklists

This commit is contained in:
Henry
2026-03-16 13:35:03 +00:00
parent b8d1fe230d
commit 4088f9abd5
3 changed files with 30 additions and 24 deletions

View File

@@ -4,6 +4,7 @@ import { z } from "zod";
import * as cardRepo from "@kan/db/repository/card.repo"; import * as cardRepo from "@kan/db/repository/card.repo";
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo"; import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
import * as checklistRepo from "@kan/db/repository/checklist.repo"; import * as checklistRepo from "@kan/db/repository/checklist.repo";
import { stripHtml } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { assertPermission } from "../utils/permissions"; import { assertPermission } from "../utils/permissions";
@@ -223,7 +224,7 @@ export const checklistRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
checklistPublicId: z.string().length(12), checklistPublicId: z.string().length(12),
title: z.string().min(1).max(500), title: z.string().min(1).max(500).transform(stripHtml),
}), }),
) )
.output(checklistItemSchema) .output(checklistItemSchema)
@@ -288,7 +289,7 @@ export const checklistRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
checklistItemPublicId: z.string().length(12), checklistItemPublicId: z.string().length(12),
title: z.string().min(1).max(500).optional(), title: z.string().min(1).max(500).transform(stripHtml).optional(),
completed: z.boolean().optional(), completed: z.boolean().optional(),
index: z.number().int().min(0).optional(), index: z.number().int().min(0).optional(),
}), }),
@@ -322,30 +323,29 @@ export const checklistRouter = createTRPCRouter({
const previousTitle = item.title; const previousTitle = item.title;
let updatedItem; let updatedItem;
if (input.title !== undefined || input.completed !== undefined) { if (input.title !== undefined || input.completed !== undefined) {
updatedItem = await checklistRepo.updateItemById(ctx.db, { updatedItem = await checklistRepo.updateItemById(ctx.db, {
id: item.id, id: item.id,
title: input.title, title: input.title,
completed: input.completed, completed: input.completed,
}); });
} }
if (input.index !== undefined) { if (input.index !== undefined) {
updatedItem = await checklistRepo.reorderItem(ctx.db, { updatedItem = await checklistRepo.reorderItem(ctx.db, {
itemId: item.id, itemId: item.id,
newIndex: input.index, newIndex: input.index,
}); });
} }
if (!updatedItem) {
throw new TRPCError({
message: `Failed to update checklist item`,
code: "INTERNAL_SERVER_ERROR",
});
}
if (!updatedItem) {
throw new TRPCError({
message: `Failed to update checklist item`,
code: "INTERNAL_SERVER_ERROR",
});
}
// Log completion toggle // Log completion toggle
if (input.completed !== undefined) { if (input.completed !== undefined) {
@@ -371,7 +371,6 @@ export const checklistRouter = createTRPCRouter({
} }
return updatedItem; return updatedItem;
}), }),
deleteItem: protectedProcedure deleteItem: protectedProcedure
.meta({ .meta({

View File

@@ -5,3 +5,4 @@ export * from "./email";
export * from "./dueDateFilters"; export * from "./dueDateFilters";
export * from "./s3"; export * from "./s3";
export * from "./mentions"; export * from "./mentions";
export * from "./sanitize";

View File

@@ -0,0 +1,6 @@
/**
* Strips all HTML tags from a string, returning plain text.
* Use this on any user-supplied text field before storing or displaying.
*/
export const stripHtml = (value: string): string =>
value.replace(/<[^>]*>/g, "").trim();