diff --git a/package.json b/package.json index 133f1e81d..58ee7ec5a 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "uuid": "^8.3.2", "xml-js": "^1.6.11", "yarn": "^1.22.19", + "zod": "^3.21.4", "zustand": "^4.1.4" }, "devDependencies": { diff --git a/src/hooks/widgets/rss/useGetRssFeed.tsx b/src/hooks/widgets/rss/useGetRssFeed.tsx index 2fc9e07e3..d96176466 100644 --- a/src/hooks/widgets/rss/useGetRssFeed.tsx +++ b/src/hooks/widgets/rss/useGetRssFeed.tsx @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query'; -export const useGetRssFeed = (feedUrl: string) => +export const useGetRssFeed = (feedUrl: string, widgetId: string) => useQuery({ queryKey: ['rss-feed', feedUrl], queryFn: async () => { - const response = await fetch('/api/modules/rss'); + const response = await fetch(`/api/modules/rss?widgetId=${widgetId}`); return response.json(); }, }); diff --git a/src/pages/api/modules/calendar.ts b/src/pages/api/modules/calendar.ts index 77ef77f57..668d667a9 100644 --- a/src/pages/api/modules/calendar.ts +++ b/src/pages/api/modules/calendar.ts @@ -4,6 +4,7 @@ import Consola from 'consola'; import { NextApiRequest, NextApiResponse } from 'next'; +import { z } from 'zod'; import { AppIntegrationType } from '../../../types/app'; import { getConfig } from '../../../tools/config/getConfig'; @@ -18,28 +19,36 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { }); }; +const getQuerySchema = z.object({ + month: z + .string() + .regex(/^\d+$/) + .transform((x) => parseInt(x, 10)), + year: z + .string() + .regex(/^\d+$/) + .transform((x) => parseInt(x, 10)), + widgetId: z.string().uuid(), + configName: z.string(), +}); + async function Get(req: NextApiRequest, res: NextApiResponse) { - // Parse req.body as a AppItem - const { - month: monthString, - year: yearString, - configName, - } = req.query as { month: string; year: string; configName: string }; + const parseResult = getQuerySchema.safeParse(req.query); - const month = parseInt(monthString, 10); - const year = parseInt(yearString, 10); - - if (Number.isNaN(month) || Number.isNaN(year) || !configName) { + if (!parseResult.success) { return res.status(400).json({ statusCode: 400, - message: 'Missing required parameter in url: year, month or configName', + message: 'Invalid query parameters, please specify the widgetId, month, year and configName', }); } + // Parse req.body as a AppItem + const { month, year, widgetId, configName } = parseResult.data; + const config = getConfig(configName); // Find the calendar widget in the config - const calendar = config.widgets.find((w) => w.type === 'calendar'); + const calendar = config.widgets.find((w) => w.type === 'calendar' && w.id === widgetId); const useSonarrv4 = calendar?.properties.useSonarrv4 ?? false; const mediaAppIntegrationTypes: AppIntegrationType['type'][] = [ diff --git a/src/pages/api/modules/dashdot/info.ts b/src/pages/api/modules/dashdot/info.ts index 7334944b1..1f7b178a0 100644 --- a/src/pages/api/modules/dashdot/info.ts +++ b/src/pages/api/modules/dashdot/info.ts @@ -1,20 +1,29 @@ import axios from 'axios'; import { NextApiRequest, NextApiResponse } from 'next'; +import { z } from 'zod'; import { getConfig } from '../../../../tools/config/getConfig'; import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile'; -async function Get(req: NextApiRequest, res: NextApiResponse) { - const { configName } = req.query; +const getQuerySchema = z.object({ + configName: z.string(), + widgetId: z.string().uuid(), +}); - if (!configName || typeof configName !== 'string') { +async function Get(req: NextApiRequest, res: NextApiResponse) { + const parseResult = getQuerySchema.safeParse(req.query); + + if (!parseResult.success) { return res.status(400).json({ - message: 'Missing required configName in url', + statusCode: 400, + message: 'Invalid query parameters, please specify the widgetId and configName', }); } + const { configName, widgetId } = parseResult.data; + const config = getConfig(configName); - const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot'); + const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot' && x.id === widgetId); if (!dashDotWidget) { return res.status(400).json({ diff --git a/src/pages/api/modules/dashdot/storage.ts b/src/pages/api/modules/dashdot/storage.ts index b2952f431..30b188c30 100644 --- a/src/pages/api/modules/dashdot/storage.ts +++ b/src/pages/api/modules/dashdot/storage.ts @@ -1,19 +1,28 @@ import axios from 'axios'; import { NextApiRequest, NextApiResponse } from 'next'; +import { z } from 'zod'; import { getConfig } from '../../../../tools/config/getConfig'; import { IDashDotTile } from '../../../../widgets/dashDot/DashDotTile'; -async function Get(req: NextApiRequest, res: NextApiResponse) { - const { configName } = req.query; +const getQuerySchema = z.object({ + configName: z.string(), + widgetId: z.string().uuid(), +}); - if (!configName || typeof configName !== 'string') { +async function Get(req: NextApiRequest, res: NextApiResponse) { + const parseResult = getQuerySchema.safeParse(req.query); + + if (!parseResult.success) { return res.status(400).json({ - message: 'Missing required configName in url', + statusCode: 400, + message: 'Invalid query parameters, please specify the widgetId and configName', }); } + const { configName, widgetId } = parseResult.data; + const config = getConfig(configName); - const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot'); + const dashDotWidget = config.widgets.find((x) => x.type === 'dashdot' && x.id === widgetId); if (!dashDotWidget) { return res.status(400).json({ diff --git a/src/pages/api/modules/rss/index.ts b/src/pages/api/modules/rss/index.ts index f76011497..7ea57572b 100644 --- a/src/pages/api/modules/rss/index.ts +++ b/src/pages/api/modules/rss/index.ts @@ -8,6 +8,7 @@ import { NextApiRequest, NextApiResponse } from 'next'; import Parser from 'rss-parser'; +import { z } from 'zod'; import { getConfig } from '../../../../tools/config/getConfig'; import { IRssWidget } from '../../../../widgets/rss/RssWidgetTile'; import { Stopwatch } from '../../../../tools/shared/time/stopwatch.tool'; @@ -25,11 +26,24 @@ const parser: Parser = new Parser({ }, }); +const getQuerySchema = z.object({ + widgetId: z.string().uuid(), +}); + export const Get = async (request: NextApiRequest, response: NextApiResponse) => { const configName = getCookie('config-name', { req: request }); const config = getConfig(configName?.toString() ?? 'default'); - const rssWidget = config.widgets.find((x) => x.type === 'rss') as IRssWidget | undefined; + const parseResult = getQuerySchema.safeParse(request.query); + + if (!parseResult.success) { + response.status(400).json({ message: 'invalid query parameters, please specify the widgetId' }); + return; + } + + const rssWidget = config.widgets.find( + (x) => x.type === 'rss' && x.id === parseResult.data.widgetId + ) as IRssWidget | undefined; if ( !rssWidget || diff --git a/src/widgets/calendar/CalendarTile.tsx b/src/widgets/calendar/CalendarTile.tsx index 3c812599b..389aa7e5f 100644 --- a/src/widgets/calendar/CalendarTile.tsx +++ b/src/widgets/calendar/CalendarTile.tsx @@ -63,7 +63,7 @@ function CalendarTile({ widget }: CalendarTileProps) { await fetch( `/api/modules/calendar?year=${month.getFullYear()}&month=${ month.getMonth() + 1 - }&configName=${configName}` + }&configName=${configName}&id=${widget.id}` ) ).json()) as MediasType, }); diff --git a/src/widgets/dashDot/DashDotCompactStorage.tsx b/src/widgets/dashDot/DashDotCompactStorage.tsx index e06fe11aa..319ffe10c 100644 --- a/src/widgets/dashDot/DashDotCompactStorage.tsx +++ b/src/widgets/dashDot/DashDotCompactStorage.tsx @@ -9,11 +9,12 @@ import { DashDotInfo } from './DashDotCompactNetwork'; interface DashDotCompactStorageProps { info: DashDotInfo; + widgetId: string; } -export const DashDotCompactStorage = ({ info }: DashDotCompactStorageProps) => { +export const DashDotCompactStorage = ({ info, widgetId }: DashDotCompactStorageProps) => { const { t } = useTranslation('modules/dashdot'); - const { data: storageLoad } = useDashDotStorage(); + const { data: storageLoad } = useDashDotStorage(widgetId); const totalUsed = calculateTotalLayoutSize({ layout: storageLoad?.layout ?? [], @@ -50,7 +51,7 @@ interface CalculateTotalLayoutSizeProps { key: keyof TLayoutItem; } -const useDashDotStorage = () => { +const useDashDotStorage = (widgetId: string) => { const { name: configName, config } = useConfigContext(); return useQuery({ @@ -59,16 +60,17 @@ const useDashDotStorage = () => { { configName, url: config?.widgets.find((x) => x.type === 'dashdot')?.properties.url, + widgetId, }, ], - queryFn: () => fetchDashDotStorageLoad(configName), + queryFn: () => fetchDashDotStorageLoad(configName, widgetId), }); }; -async function fetchDashDotStorageLoad(configName: string | undefined) { +async function fetchDashDotStorageLoad(configName: string | undefined, widgetId: string) { if (!configName) throw new Error('configName is undefined'); return (await ( - await axios.get('/api/modules/dashdot/storage', { params: { configName } }) + await axios.get('/api/modules/dashdot/storage', { params: { configName, widgetId } }) ).data) as DashDotStorageLoad; } diff --git a/src/widgets/dashDot/DashDotGraph.tsx b/src/widgets/dashDot/DashDotGraph.tsx index dd9fda585..d26bd0e90 100644 --- a/src/widgets/dashDot/DashDotGraph.tsx +++ b/src/widgets/dashDot/DashDotGraph.tsx @@ -11,6 +11,7 @@ interface DashDotGraphProps { dashDotUrl: string; usePercentages: boolean; info: DashDotInfo; + widgetId: string; } export const DashDotGraph = ({ @@ -21,12 +22,13 @@ export const DashDotGraph = ({ dashDotUrl, usePercentages, info, + widgetId, }: DashDotGraphProps) => { const { t } = useTranslation('modules/dashdot'); const { classes } = useStyles(); if (graph === 'storage' && isCompact) { - return ; + return ; } if (graph === 'network' && isCompact) { diff --git a/src/widgets/dashDot/DashDotTile.tsx b/src/widgets/dashDot/DashDotTile.tsx index 33bdf820d..fe19b0110 100644 --- a/src/widgets/dashDot/DashDotTile.tsx +++ b/src/widgets/dashDot/DashDotTile.tsx @@ -160,6 +160,7 @@ function DashDotTile({ widget }: DashDotTileProps) { const { data: info } = useDashDotInfo({ dashDotUrl, enabled: !detectedProtocolDowngrade, + widgetId: widget.id, }); if (detectedProtocolDowngrade) { @@ -197,6 +198,7 @@ function DashDotTile({ widget }: DashDotTileProps) { isCompact={g.subValues.compactView ?? false} multiView={g.subValues.multiView ?? false} usePercentages={usePercentages} + widgetId={widget.id} /> ))} @@ -207,7 +209,15 @@ function DashDotTile({ widget }: DashDotTileProps) { ); } -const useDashDotInfo = ({ dashDotUrl, enabled }: { dashDotUrl: string; enabled: boolean }) => { +const useDashDotInfo = ({ + dashDotUrl, + enabled, + widgetId, +}: { + dashDotUrl: string; + enabled: boolean; + widgetId: string; +}) => { const { name: configName } = useConfigContext(); return useQuery({ refetchInterval: 50000, @@ -218,15 +228,15 @@ const useDashDotInfo = ({ dashDotUrl, enabled }: { dashDotUrl: string; enabled: dashDotUrl, }, ], - queryFn: () => fetchDashDotInfo(configName), + queryFn: () => fetchDashDotInfo(configName, widgetId), enabled, }); }; -const fetchDashDotInfo = async (configName: string | undefined) => { +const fetchDashDotInfo = async (configName: string | undefined, widgetId: string) => { if (!configName) return {} as DashDotInfo; return (await ( - await axios.get('/api/modules/dashdot/info', { params: { configName } }) + await axios.get('/api/modules/dashdot/info', { params: { configName, widgetId } }) ).data) as DashDotInfo; }; diff --git a/src/widgets/rss/RssWidgetTile.tsx b/src/widgets/rss/RssWidgetTile.tsx index 6c6e53b4e..095d58771 100644 --- a/src/widgets/rss/RssWidgetTile.tsx +++ b/src/widgets/rss/RssWidgetTile.tsx @@ -60,7 +60,8 @@ interface RssTileProps { function RssTile({ widget }: RssTileProps) { const { t } = useTranslation('modules/rss'); const { data, isLoading, isFetching, isError, refetch } = useGetRssFeed( - widget.properties.rssFeedUrl + widget.properties.rssFeedUrl, + widget.id ); const { classes } = useStyles(); const [loadingOverlayVisible, setLoadingOverlayVisible] = useState(false); diff --git a/yarn.lock b/yarn.lock index 04d9d2160..53d9a46ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4914,6 +4914,7 @@ __metadata: vitest-fetch-mock: ^0.2.2 xml-js: ^1.6.11 yarn: ^1.22.19 + zod: ^3.21.4 zustand: ^4.1.4 languageName: unknown linkType: soft @@ -8760,6 +8761,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.21.4": + version: 3.21.4 + resolution: "zod@npm:3.21.4" + checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f + languageName: node + linkType: hard + "zustand@npm:^4.1.4": version: 4.3.6 resolution: "zustand@npm:4.3.6"