diff --git a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/(list)/page.tsx b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/(list)/page.tsx index 75c54406f..0658fb2f5 100644 --- a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/(list)/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/(list)/page.tsx @@ -3,6 +3,7 @@ import { Button } from "@openstatus/ui/components/ui/button"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useParams, useRouter } from "next/navigation"; +import { toast } from "sonner"; import { Link } from "@/components/common/link"; import { @@ -18,6 +19,7 @@ import { FormSheetMaintenance } from "@/components/forms/maintenance/sheet"; import { toCheckboxTreeItems } from "@/components/ui/checkbox-tree"; import { DataTable } from "@/components/ui/data-table/data-table"; import { useTRPC } from "@/lib/trpc/client"; +import { errorMessage } from "@/lib/trpc/error"; export default function Page() { const { id } = useParams<{ id: string }>(); @@ -33,7 +35,11 @@ export default function Page() { }), ); const sendMaintenanceUpdateMutation = useMutation( - trpc.subscriberNotification.maintenance.mutationOptions(), + trpc.subscriberNotification.maintenance.mutationOptions({ + onError: (error) => { + toast.error(errorMessage(error, "Failed to notify subscribers")); + }, + }), ); const createMaintenanceMutation = useMutation( trpc.maintenance.new.mutationOptions({ diff --git a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/[maintenanceId]/client.tsx b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/[maintenanceId]/client.tsx index 0223f4ad4..1df67cf23 100644 --- a/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/[maintenanceId]/client.tsx +++ b/apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/[maintenanceId]/client.tsx @@ -50,7 +50,11 @@ export function Client({ id, pageId }: { id: number; pageId: number }) { toast.error(errorMessage(error, "Failed to save")); }; const notify = useMutation( - trpc.subscriberNotification.maintenanceUpdate.mutationOptions(), + trpc.subscriberNotification.maintenanceUpdate.mutationOptions({ + onError: (error) => { + toast.error(errorMessage(error, "Failed to notify subscribers")); + }, + }), ); const createUpdate = useMutation( trpc.maintenance.createUpdate.mutationOptions({ diff --git a/apps/dashboard/src/components/forms/maintenance/sheet-create.tsx b/apps/dashboard/src/components/forms/maintenance/sheet-create.tsx index 16c1f9232..00384bd1d 100644 --- a/apps/dashboard/src/components/forms/maintenance/sheet-create.tsx +++ b/apps/dashboard/src/components/forms/maintenance/sheet-create.tsx @@ -12,6 +12,7 @@ import { import { Separator } from "@openstatus/ui/components/ui/separator"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; +import { toast } from "sonner"; import { EmptyStateContainer, @@ -30,6 +31,7 @@ import { import { FormMaintenance } from "@/components/forms/maintenance/form"; import { toCheckboxTreeItems } from "@/components/ui/checkbox-tree"; import { useTRPC } from "@/lib/trpc/client"; +import { errorMessage } from "@/lib/trpc/error"; /** * Create-maintenance sheet with a status page selector — for surfaces that @@ -67,7 +69,11 @@ export function FormSheetMaintenanceCreate({ ); const sendMaintenanceUpdateMutation = useMutation( - trpc.subscriberNotification.maintenance.mutationOptions(), + trpc.subscriberNotification.maintenance.mutationOptions({ + onError: (error) => { + toast.error(errorMessage(error, "Failed to notify subscribers")); + }, + }), ); const createMaintenanceMutation = useMutation( trpc.maintenance.new.mutationOptions({ diff --git a/apps/server/src/routes/rpc/handlers/maintenance/__tests__/maintenance.test.ts b/apps/server/src/routes/rpc/handlers/maintenance/__tests__/maintenance.test.ts index 51aa8bcc5..7b70ae368 100644 --- a/apps/server/src/routes/rpc/handlers/maintenance/__tests__/maintenance.test.ts +++ b/apps/server/src/routes/rpc/handlers/maintenance/__tests__/maintenance.test.ts @@ -396,6 +396,28 @@ describe("MaintenanceService.CreateMaintenance", () => { expect(res.status).toBe(404); }); + test("rejects page component ids that are not plain decimal digits", async () => { + const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000); + + for (const id of ["1e3", "0x10", "1.5", ""]) { + const res = await connectRequest( + "CreateMaintenance", + { + title: "Malformed component id", + message: "Test message", + from: fromDate.toISOString(), + to: toDate.toISOString(), + pageId: String(testPageId), + pageComponentIds: [id], + }, + { "x-openstatus-key": authKey }, + ); + expect(res.status).toBe(400); + expect((await res.json()).code).toBe("invalid_argument"); + } + }); + test("returns error when page components are from different pages", async () => { const fromDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); const toDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000 + 3600000); diff --git a/apps/server/src/routes/rpc/handlers/maintenance/index.ts b/apps/server/src/routes/rpc/handlers/maintenance/index.ts index 85c5f17c5..fbfabad94 100644 --- a/apps/server/src/routes/rpc/handlers/maintenance/index.ts +++ b/apps/server/src/routes/rpc/handlers/maintenance/index.ts @@ -34,16 +34,20 @@ function parseDate(dateString: string): Date { return date; } +// `Number("1e3")` is 1000 and `Number("0x10")` is 16 — both would silently +// target another component, so only plain decimal digits are accepted. +const PAGE_COMPONENT_ID = /^\d+$/; + function parsePageComponentIds(ids: ReadonlyArray): number[] { return ids.map((id) => { - const n = Number(id); - if (id.trim() === "" || !Number.isSafeInteger(n)) { + const trimmed = id.trim(); + if (!PAGE_COMPONENT_ID.test(trimmed)) { throw new ConnectError( `Invalid page component id: "${id}"`, Code.InvalidArgument, ); } - return n; + return Number(trimmed); }); }