From 2b08a5a9663b0c002143e5300c1621717ec06a45 Mon Sep 17 00:00:00 2001
From: Ivan <8611739+IRBorisov@users.noreply.github.com>
Date: Thu, 30 Jan 2025 19:23:15 +0300
Subject: [PATCH] F: Add zod validation for query params
---
README.md | 1 +
rsconcept/frontend/package-lock.json | 2 +-
rsconcept/frontend/package.json | 1 +
rsconcept/frontend/src/app/urls.ts | 28 ++++++-------
rsconcept/frontend/src/pages/LoginPage.tsx | 3 +-
.../src/pages/ManualsPage/ManualsPage.tsx | 2 +-
.../frontend/src/pages/OssPage/OssPage.tsx | 30 +++++++++++---
.../frontend/src/pages/OssPage/OssTabs.tsx | 15 +++----
.../src/pages/RSFormPage/RSFormPage.tsx | 39 ++++++++++++++-----
.../frontend/src/pages/RSFormPage/RSTabs.tsx | 21 +++++-----
10 files changed, 88 insertions(+), 54 deletions(-)
diff --git a/README.md b/README.md
index 3a84bd47..f8576165 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ This readme file is used mostly to document project dependencies and conventions
- qrcode.react
- html-to-image
- zustand
+ - zod
- @tanstack/react-table
- @tanstack/react-query
- @tanstack/react-query-devtools
diff --git a/rsconcept/frontend/package-lock.json b/rsconcept/frontend/package-lock.json
index f5ea912a..774100ff 100644
--- a/rsconcept/frontend/package-lock.json
+++ b/rsconcept/frontend/package-lock.json
@@ -33,6 +33,7 @@
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"use-debounce": "^10.0.4",
+ "zod": "^3.24.1",
"zustand": "^5.0.3"
},
"devDependencies": {
@@ -10932,7 +10933,6 @@
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json
index ebedd53c..1b604886 100644
--- a/rsconcept/frontend/package.json
+++ b/rsconcept/frontend/package.json
@@ -37,6 +37,7 @@
"react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4",
"use-debounce": "^10.0.4",
+ "zod": "^3.24.1",
"zustand": "^5.0.3"
},
"devDependencies": {
diff --git a/rsconcept/frontend/src/app/urls.ts b/rsconcept/frontend/src/app/urls.ts
index d36ef301..a2e93cab 100644
--- a/rsconcept/frontend/src/app/urls.ts
+++ b/rsconcept/frontend/src/app/urls.ts
@@ -24,18 +24,6 @@ export const routes = {
database_schema: 'database-schema'
};
-interface SchemaProps {
- id: number | string;
- tab: number;
- version?: number | string;
- active?: number | string;
-}
-
-interface OssProps {
- id: number | string;
- tab: number;
-}
-
/**
* Internal navigation URLs.
*/
@@ -58,12 +46,24 @@ export const urls = {
schema: (id: number | string, version?: number | string) =>
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
oss: (id: number | string, tab?: number) => `/oss/${id}` + (tab !== undefined ? `?tab=${tab}` : ''),
- schema_props: ({ id, tab, version, active }: SchemaProps) => {
+
+ schema_props: ({
+ id,
+ tab,
+ version,
+ active
+ }: {
+ id: number | string;
+ tab: number;
+ version?: number | string;
+ active?: number | string;
+ }) => {
const versionStr = version !== undefined ? `v=${version}&` : '';
const activeStr = active !== undefined ? `&active=${active}` : '';
return `/rsforms/${id}?${versionStr}tab=${tab}${activeStr}`;
},
- oss_props: ({ id, tab }: OssProps) => {
+
+ oss_props: ({ id, tab }: { id: number | string; tab: number }) => {
return `/oss/${id}?tab=${tab}`;
}
};
diff --git a/rsconcept/frontend/src/pages/LoginPage.tsx b/rsconcept/frontend/src/pages/LoginPage.tsx
index f3f73026..519b93bd 100644
--- a/rsconcept/frontend/src/pages/LoginPage.tsx
+++ b/rsconcept/frontend/src/pages/LoginPage.tsx
@@ -19,12 +19,11 @@ import { resources } from '@/utils/constants';
function LoginPage() {
const router = useConceptNavigation();
const query = useQueryStrings();
- const userQuery = query.get('username');
const { isAnonymous } = useAuthSuspense();
const { login, isPending, error, reset } = useLogin();
- const [username, setUsername] = useState(userQuery || '');
+ const [username, setUsername] = useState(query.get('username') ?? '');
const [password, setPassword] = useState('');
useEffect(() => {
diff --git a/rsconcept/frontend/src/pages/ManualsPage/ManualsPage.tsx b/rsconcept/frontend/src/pages/ManualsPage/ManualsPage.tsx
index 8de7bbb0..8f1e50e7 100644
--- a/rsconcept/frontend/src/pages/ManualsPage/ManualsPage.tsx
+++ b/rsconcept/frontend/src/pages/ManualsPage/ManualsPage.tsx
@@ -13,7 +13,7 @@ import ViewTopic from './ViewTopic';
export function ManualsPage() {
const router = useConceptNavigation();
const query = useQueryStrings();
- const activeTopic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic;
+ const activeTopic = (query.get('topic') ?? HelpTopic.MAIN) as HelpTopic;
const mainHeight = useMainHeight();
diff --git a/rsconcept/frontend/src/pages/OssPage/OssPage.tsx b/rsconcept/frontend/src/pages/OssPage/OssPage.tsx
index 30bebf4f..1ddb95e8 100644
--- a/rsconcept/frontend/src/pages/OssPage/OssPage.tsx
+++ b/rsconcept/frontend/src/pages/OssPage/OssPage.tsx
@@ -1,35 +1,53 @@
'use client';
import axios from 'axios';
+import { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router';
+import { z } from 'zod';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { ErrorData } from '@/components/info/InfoError';
import TextURL from '@/components/ui/TextURL';
+import useQueryStrings from '@/hooks/useQueryStrings';
import { useModificationStore } from '@/stores/modification';
-import { OssEditState } from './OssEditContext';
+import { OssEditState, OssTabID } from './OssEditContext';
import OssTabs from './OssTabs';
+const paramsSchema = z.object({
+ id: z
+ .string()
+ .nullish()
+ .transform(v => (v ? Number(v) : undefined)),
+ tab: z.preprocess(v => (v ? Number(v) : undefined), z.nativeEnum(OssTabID).default(OssTabID.CARD))
+});
+
export function OssPage() {
const router = useConceptNavigation();
const params = useParams();
- const itemID = params.id ? Number(params.id) : undefined;
+ const query = useQueryStrings();
- const { isModified } = useModificationStore();
+ const urlData = paramsSchema.parse({
+ id: params.id,
+ tab: query.get('tab')
+ });
+
+ const { isModified, setIsModified } = useModificationStore();
useBlockNavigation(isModified);
- if (!itemID) {
+ useEffect(() => setIsModified(false), [setIsModified]);
+
+ if (!urlData.id) {
router.replace(urls.page404);
return null;
}
return (
-
-
+
+
);
diff --git a/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx b/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx
index e0bbc9c6..6dd3eef7 100644
--- a/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx
+++ b/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx
@@ -7,28 +7,23 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
-import useQueryStrings from '@/hooks/useQueryStrings';
import { useAppLayoutStore } from '@/stores/appLayout';
-import { useModificationStore } from '@/stores/modification';
import EditorRSForm from './EditorOssCard';
import EditorTermGraph from './EditorOssGraph';
import MenuOssTabs from './MenuOssTabs';
import { OssTabID, useOssEdit } from './OssEditContext';
-function OssTabs() {
- const router = useConceptNavigation();
- const query = useQueryStrings();
- const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
+interface OssTabsProps {
+ activeTab: OssTabID;
+}
+function OssTabs({ activeTab }: OssTabsProps) {
+ const router = useConceptNavigation();
const { schema, navigateTab } = useOssEdit();
const hideFooter = useAppLayoutStore(state => state.hideFooter);
- const { setIsModified } = useModificationStore();
-
- useEffect(() => setIsModified(false), [setIsModified]);
-
useEffect(() => {
const oldTitle = document.title;
document.title = schema.title;
diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSFormPage.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSFormPage.tsx
index 39285576..0d83bb77 100644
--- a/rsconcept/frontend/src/pages/RSFormPage/RSFormPage.tsx
+++ b/rsconcept/frontend/src/pages/RSFormPage/RSFormPage.tsx
@@ -1,8 +1,10 @@
'use client';
import axios from 'axios';
+import { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router';
+import { z } from 'zod';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
@@ -16,29 +18,48 @@ import { useModificationStore } from '@/stores/modification';
import { RSEditState, RSTabID } from './RSEditContext';
import RSTabs from './RSTabs';
+const paramsSchema = z.object({
+ id: z
+ .string()
+ .nullish()
+ .transform(v => (v ? Number(v) : undefined)),
+ version: z
+ .string()
+ .nullish()
+ .transform(v => (v ? Number(v) : undefined)),
+ tab: z.preprocess(v => (v ? Number(v) : undefined), z.nativeEnum(RSTabID).default(RSTabID.CARD)),
+ activeID: z.preprocess(v => (v ? Number(v) : undefined), z.number().optional())
+});
+
export function RSFormPage() {
const router = useConceptNavigation();
- const query = useQueryStrings();
const params = useParams();
- const itemID = params.id ? Number(params.id) : undefined;
- const version = query.get('v') ? Number(query.get('v')) : undefined;
- const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
+ const query = useQueryStrings();
- const { isModified } = useModificationStore();
+ const urlData = paramsSchema.parse({
+ id: params.id,
+ version: query.get('v'),
+ tab: query.get('tab'),
+ activeID: query.get('active')
+ });
+
+ const { isModified, setIsModified } = useModificationStore();
useBlockNavigation(isModified);
- if (!itemID) {
+ useEffect(() => setIsModified(false), [setIsModified]);
+
+ if (!urlData.id) {
router.replace(urls.page404);
return null;
}
return (
(
-
+
)}
>
-
-
+
+
);
diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx
index 7866ec02..6770c98d 100644
--- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx
+++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx
@@ -7,7 +7,7 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
-import useQueryStrings from '@/hooks/useQueryStrings';
+import { ConstituentaID } from '@/models/rsform';
import { useAppLayoutStore } from '@/stores/appLayout';
import { useModificationStore } from '@/stores/modification';
import { labelVersion } from '@/utils/labels';
@@ -19,18 +19,18 @@ import EditorTermGraph from './EditorTermGraph';
import MenuRSTabs from './MenuRSTabs';
import { RSTabID, useRSEdit } from './RSEditContext';
-function RSTabs() {
- const query = useQueryStrings();
+interface RSTabsProps {
+ activeID?: ConstituentaID;
+ activeTab: RSTabID;
+}
+
+function RSTabs({ activeID, activeTab }: RSTabsProps) {
const router = useConceptNavigation();
- const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
- const cstQuery = query.get('active');
const hideFooter = useAppLayoutStore(state => state.hideFooter);
const { setIsModified } = useModificationStore();
const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
- useEffect(() => setIsModified(false), [setIsModified]);
-
useEffect(() => {
const oldTitle = document.title;
document.title = schema.title;
@@ -43,15 +43,14 @@ function RSTabs() {
hideFooter(activeTab !== RSTabID.CARD);
setIsModified(false);
if (activeTab === RSTabID.CST_EDIT) {
- const cstID = Number(cstQuery);
- if (cstID && schema.cstByID.has(cstID)) {
- setSelected([cstID]);
+ if (activeID && schema.cstByID.has(activeID)) {
+ setSelected([activeID]);
} else {
setSelected([]);
}
}
return () => hideFooter(false);
- }, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]);
+ }, [activeTab, activeID, setSelected, schema, hideFooter, setIsModified]);
function onSelectTab(index: number, last: number, event: Event) {
if (last === index) {