From 5326e9f61f599aaa16207decb7e9d11932459a2f Mon Sep 17 00:00:00 2001 From: nakaterm <104970808+nakaterm@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:52:15 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=97=A5=E7=A8=8B=E7=AF=84=E5=9B=B2?= =?UTF-8?q?=E5=A4=96=E3=82=B9=E3=83=AD=E3=83=83=E3=83=88=E3=81=AB=E3=82=88?= =?UTF-8?q?=E3=82=8B=E3=82=AB=E3=83=AC=E3=83=B3=E3=83=80=E3=83=BC=E6=8F=8F?= =?UTF-8?q?=E7=94=BB=E3=82=AF=E3=83=A9=E3=83=83=E3=82=B7=E3=83=A5=E3=82=92?= =?UTF-8?q?=E9=98=B2=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DB に範囲外の Slot が存在する場合 CalendarMatrix の行列アクセスが out of bounds になり画面がクラッシュしていたため、行列境界チェックを 追加して該当セルを無視するようにした。また、提出/更新エンドポイントで プロジェクトの日程範囲外のスロットをサーバー側で拒否し、今後同様の データが作られないようにした。 --- client/src/lib/CalendarMatrix.ts | 7 +++++++ server/src/routes/projects.ts | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/client/src/lib/CalendarMatrix.ts b/client/src/lib/CalendarMatrix.ts index cb24741..593d892 100644 --- a/client/src/lib/CalendarMatrix.ts +++ b/client/src/lib/CalendarMatrix.ts @@ -34,8 +34,13 @@ abstract class CalendarMatrixBase { return [dayDiff, Math.floor(totalMinutes / 15)]; } + protected isInBounds(row: number, col: number): boolean { + return row >= 0 && row < this.matrix.length && col >= 0 && col < this.quarterCount; + } + getIsSlotExist(dt: Dayjs): boolean { const [row, col] = this.getIndex(dt); + if (!this.isInBounds(row, col)) return false; return this.matrix[row][col] !== null; } @@ -44,6 +49,7 @@ abstract class CalendarMatrixBase { const [endRow, endCol] = this.getIndex(to.subtract(1, "minute")); for (let r = startRow; r <= endRow; r++) { for (let c = startCol; c <= endCol; c++) { + if (!this.isInBounds(r, c)) continue; this.matrix[r][c] = newValue; } } @@ -89,6 +95,7 @@ export class ViewingMatrix extends CalendarMatrixBase> { const [endRow, endCol] = this.getIndex(to.subtract(1, "minute")); for (let r = startRow; r <= endRow; r++) { for (let c = startCol; c <= endCol; c++) { + if (!this.isInBounds(r, c)) continue; if (this.matrix[r][c] === null) { this.matrix[r][c] = {}; } diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index d4b0bc7..4fa1155 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -9,6 +9,20 @@ dotenv.config(); const projectIdParamsSchema = z.object({ projectId: z.string().length(21) }); +/** + * 提出されたスロットがプロジェクトの日程範囲内に収まっているか検証する。 + * startDate/endDate は日付部分のみ意味を持つため、endDate の翌日 0 時を終端(exclusive)として扱う。 + */ +function areSlotsWithinProjectRange( + slots: { start: Date; end: Date }[] | undefined, + startDate: Date, + endDate: Date, +): boolean { + if (!slots) return true; + const rangeEnd = new Date(endDate.getTime() + 24 * 60 * 60 * 1000); + return slots.every((slot) => slot.start < slot.end && slot.start >= startDate && slot.end <= rangeEnd); +} + const router = new Hono<{ Variables: AppVariables }>() // プロジェクト作成 .post("/", zValidator("json", projectReqSchema), async (c) => { @@ -256,6 +270,17 @@ const router = new Hono<{ Variables: AppVariables }>() const { projectId } = c.req.valid("param"); const { name, comment, slots } = c.req.valid("json"); + const project = await prisma.project.findUnique({ + where: { id: projectId }, + select: { startDate: true, endDate: true }, + }); + if (!project) { + return c.json({ message: "イベントが見つかりません。" }, 404); + } + if (!areSlotsWithinProjectRange(slots, project.startDate, project.endDate)) { + return c.json({ message: "日程がイベントの期間外です。" }, 400); + } + const existingGuest = await prisma.guest.findUnique({ where: { browserId_projectId: { @@ -299,6 +324,17 @@ const router = new Hono<{ Variables: AppVariables }>() const { projectId } = c.req.valid("param"); const { name, comment, slots } = c.req.valid("json"); + const project = await prisma.project.findUnique({ + where: { id: projectId }, + select: { startDate: true, endDate: true }, + }); + if (!project) { + return c.json({ message: "イベントが見つかりません。" }, 404); + } + if (!areSlotsWithinProjectRange(slots, project.startDate, project.endDate)) { + return c.json({ message: "日程がイベントの期間外です。" }, 400); + } + const existingGuest = await prisma.guest.findUnique({ where: { browserId_projectId: { browserId, projectId } }, include: { slots: true }, From d661f6b589c50bb27e8058eab87b1035fe32429c Mon Sep 17 00:00:00 2001 From: nakaterm <104970808+nakaterm@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:09:05 +0900 Subject: [PATCH 2/2] =?UTF-8?q?revert:=20=E3=82=B5=E3=83=BC=E3=83=90?= =?UTF-8?q?=E3=83=BC=E5=81=B4=E3=81=AE=E6=97=A5=E7=A8=8B=E7=AF=84=E5=9B=B2?= =?UTF-8?q?=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92=E4=B8=80=E6=97=A6=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit フロントエンドの境界チェックのみ残し、サーバー側の検証は別途検討する。 --- server/src/routes/projects.ts | 36 ----------------------------------- 1 file changed, 36 deletions(-) diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index 4fa1155..d4b0bc7 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -9,20 +9,6 @@ dotenv.config(); const projectIdParamsSchema = z.object({ projectId: z.string().length(21) }); -/** - * 提出されたスロットがプロジェクトの日程範囲内に収まっているか検証する。 - * startDate/endDate は日付部分のみ意味を持つため、endDate の翌日 0 時を終端(exclusive)として扱う。 - */ -function areSlotsWithinProjectRange( - slots: { start: Date; end: Date }[] | undefined, - startDate: Date, - endDate: Date, -): boolean { - if (!slots) return true; - const rangeEnd = new Date(endDate.getTime() + 24 * 60 * 60 * 1000); - return slots.every((slot) => slot.start < slot.end && slot.start >= startDate && slot.end <= rangeEnd); -} - const router = new Hono<{ Variables: AppVariables }>() // プロジェクト作成 .post("/", zValidator("json", projectReqSchema), async (c) => { @@ -270,17 +256,6 @@ const router = new Hono<{ Variables: AppVariables }>() const { projectId } = c.req.valid("param"); const { name, comment, slots } = c.req.valid("json"); - const project = await prisma.project.findUnique({ - where: { id: projectId }, - select: { startDate: true, endDate: true }, - }); - if (!project) { - return c.json({ message: "イベントが見つかりません。" }, 404); - } - if (!areSlotsWithinProjectRange(slots, project.startDate, project.endDate)) { - return c.json({ message: "日程がイベントの期間外です。" }, 400); - } - const existingGuest = await prisma.guest.findUnique({ where: { browserId_projectId: { @@ -324,17 +299,6 @@ const router = new Hono<{ Variables: AppVariables }>() const { projectId } = c.req.valid("param"); const { name, comment, slots } = c.req.valid("json"); - const project = await prisma.project.findUnique({ - where: { id: projectId }, - select: { startDate: true, endDate: true }, - }); - if (!project) { - return c.json({ message: "イベントが見つかりません。" }, 404); - } - if (!areSlotsWithinProjectRange(slots, project.startDate, project.endDate)) { - return c.json({ message: "日程がイベントの期間外です。" }, 400); - } - const existingGuest = await prisma.guest.findUnique({ where: { browserId_projectId: { browserId, projectId } }, include: { slots: true },