From 03da825ab04b4ea8c2d7efc329d425e965bf6752 Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh Date: Wed, 4 Jan 2023 21:29:39 +1100 Subject: [PATCH] refactor(web): remove query-string package (#4696) This change drops the redundant query-string package and utilises native react hooks from react-router-dom. --- web/package.json | 1 - web/pnpm-lock.yaml | 28 +------------------ web/src/constants/SearchParams.ts | 8 +++++- web/src/hooks/QueryParam.ts | 7 +++++ web/src/hooks/RedirectionURL.ts | 10 ------- web/src/hooks/RequestMethod.ts | 8 ------ web/src/utils/IdentityToken.ts | 6 ---- .../RegisterOneTimePassword.tsx | 8 +++--- .../DeviceRegistration/RegisterWebauthn.tsx | 8 +++--- .../FirstFactor/FirstFactorForm.tsx | 8 +++--- web/src/views/LoginPortal/LoginPortal.tsx | 5 ++-- .../SecondFactor/OneTimePasswordMethod.tsx | 5 ++-- .../SecondFactor/PushNotificationMethod.tsx | 5 ++-- .../SecondFactor/WebauthnMethod.tsx | 5 ++-- web/src/views/LoginPortal/SignOut/SignOut.tsx | 5 ++-- .../ResetPassword/ResetPasswordStep2.tsx | 8 +++--- 16 files changed, 46 insertions(+), 79 deletions(-) create mode 100644 web/src/hooks/QueryParam.ts delete mode 100644 web/src/hooks/RedirectionURL.ts delete mode 100644 web/src/hooks/RequestMethod.ts delete mode 100644 web/src/utils/IdentityToken.ts diff --git a/web/package.json b/web/package.json index 7f74dd7f4..19049edb7 100644 --- a/web/package.json +++ b/web/package.json @@ -35,7 +35,6 @@ "i18next-browser-languagedetector": "7.0.1", "i18next-http-backend": "2.1.1", "qrcode.react": "3.1.0", - "query-string": "7.1.3", "react": "18.2.0", "react-dom": "18.2.0", "react-i18next": "12.1.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index d13cf904d..868ba3f36 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -50,7 +50,6 @@ specifiers: jest-watch-typeahead: 2.2.1 prettier: 2.8.1 qrcode.react: 3.1.0 - query-string: 7.1.3 react: 18.2.0 react-dom: 18.2.0 react-i18next: 12.1.1 @@ -84,7 +83,6 @@ dependencies: i18next-browser-languagedetector: 7.0.1 i18next-http-backend: 2.1.1 qrcode.react: 3.1.0_react@18.2.0 - query-string: 7.1.3 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-i18next: 12.1.1_25zoxyjt3sfdpelgxivbzvmrha @@ -4849,6 +4847,7 @@ packages: /decode-uri-component/0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + dev: true /dedent/0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -5676,11 +5675,6 @@ packages: to-regex-range: 5.0.1 dev: true - /filter-obj/1.1.0: - resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} - engines: {node: '>=0.10.0'} - dev: false - /find-root/1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} dev: false @@ -7991,16 +7985,6 @@ packages: react: 18.2.0 dev: false - /query-string/7.1.3: - resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} - engines: {node: '>=6'} - dependencies: - decode-uri-component: 0.2.2 - filter-obj: 1.1.0 - split-on-first: 1.1.0 - strict-uri-encode: 2.0.0 - dev: false - /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -8593,11 +8577,6 @@ packages: resolution: {integrity: sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==} dev: true - /split-on-first/1.1.0: - resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} - engines: {node: '>=6'} - dev: false - /split-string/3.1.0: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} engines: {node: '>=0.10.0'} @@ -8630,11 +8609,6 @@ packages: object-copy: 0.1.0 dev: true - /strict-uri-encode/2.0.0: - resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} - engines: {node: '>=4'} - dev: false - /string-length/4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} diff --git a/web/src/constants/SearchParams.ts b/web/src/constants/SearchParams.ts index c23f1a2f6..bf5636151 100644 --- a/web/src/constants/SearchParams.ts +++ b/web/src/constants/SearchParams.ts @@ -1 +1,7 @@ -export const Identifier = "id"; +export const Identifier: string = "id"; + +export const IdentityToken: string = "token"; + +export const RedirectionURL: string = "rd"; + +export const RequestMethod: string = "rm"; diff --git a/web/src/hooks/QueryParam.ts b/web/src/hooks/QueryParam.ts new file mode 100644 index 000000000..758a142b9 --- /dev/null +++ b/web/src/hooks/QueryParam.ts @@ -0,0 +1,7 @@ +import { useSearchParams } from "react-router-dom"; + +export function useQueryParam(queryParam: string) { + const [searchParams] = useSearchParams(); + const value = searchParams.get(queryParam); + return value !== "" ? (value as string) : undefined; +} diff --git a/web/src/hooks/RedirectionURL.ts b/web/src/hooks/RedirectionURL.ts deleted file mode 100644 index 9f5369ffa..000000000 --- a/web/src/hooks/RedirectionURL.ts +++ /dev/null @@ -1,10 +0,0 @@ -import queryString from "query-string"; -import { useLocation } from "react-router-dom"; - -export function useRedirectionURL() { - const location = useLocation(); - - const queryParams = queryString.parse(location.search); - - return queryParams && "rd" in queryParams ? (queryParams["rd"] as string) : undefined; -} diff --git a/web/src/hooks/RequestMethod.ts b/web/src/hooks/RequestMethod.ts deleted file mode 100644 index 533b25d4c..000000000 --- a/web/src/hooks/RequestMethod.ts +++ /dev/null @@ -1,8 +0,0 @@ -import queryString from "query-string"; -import { useLocation } from "react-router-dom"; - -export function useRequestMethod() { - const location = useLocation(); - const queryParams = queryString.parse(location.search); - return queryParams && "rm" in queryParams ? (queryParams["rm"] as string) : undefined; -} diff --git a/web/src/utils/IdentityToken.ts b/web/src/utils/IdentityToken.ts deleted file mode 100644 index 7e1960015..000000000 --- a/web/src/utils/IdentityToken.ts +++ /dev/null @@ -1,6 +0,0 @@ -import queryString from "query-string"; - -export function extractIdentityToken(locationSearch: string) { - const queryParams = queryString.parse(locationSearch); - return queryParams && "token" in queryParams ? (queryParams["token"] as string) : null; -} diff --git a/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx b/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx index 9940a5d5a..289fb9779 100644 --- a/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx +++ b/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx @@ -8,20 +8,20 @@ import makeStyles from "@mui/styles/makeStyles"; import classnames from "classnames"; import { QRCodeSVG } from "qrcode.react"; import { useTranslation } from "react-i18next"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import AppStoreBadges from "@components/AppStoreBadges"; import { GoogleAuthenticator } from "@constants/constants"; import { IndexRoute } from "@constants/Routes"; +import { IdentityToken } from "@constants/SearchParams"; import { useNotifications } from "@hooks/NotificationsContext"; +import { useQueryParam } from "@hooks/QueryParam"; import LoginLayout from "@layouts/LoginLayout"; import { completeTOTPRegistrationProcess } from "@services/RegisterDevice"; -import { extractIdentityToken } from "@utils/IdentityToken"; const RegisterOneTimePassword = function () { const styles = useStyles(); const navigate = useNavigate(); - const location = useLocation(); // The secret retrieved from the API is all is ok. const [secretURL, setSecretURL] = useState("empty"); const [secretBase32, setSecretBase32] = useState(undefined as string | undefined); @@ -32,7 +32,7 @@ const RegisterOneTimePassword = function () { // Get the token from the query param to give it back to the API when requesting // the secret for OTP. - const processToken = extractIdentityToken(location.search); + const processToken = useQueryParam(IdentityToken); const handleDoneClick = () => { navigate(IndexRoute); diff --git a/web/src/views/DeviceRegistration/RegisterWebauthn.tsx b/web/src/views/DeviceRegistration/RegisterWebauthn.tsx index ec66f0f86..055479047 100644 --- a/web/src/views/DeviceRegistration/RegisterWebauthn.tsx +++ b/web/src/views/DeviceRegistration/RegisterWebauthn.tsx @@ -2,24 +2,24 @@ import React, { useCallback, useEffect, useState } from "react"; import { Button, Theme, Typography } from "@mui/material"; import makeStyles from "@mui/styles/makeStyles"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import FingerTouchIcon from "@components/FingerTouchIcon"; +import { IdentityToken } from "@constants/SearchParams"; import { useNotifications } from "@hooks/NotificationsContext"; +import { useQueryParam } from "@hooks/QueryParam"; import LoginLayout from "@layouts/LoginLayout"; import { AttestationResult } from "@models/Webauthn"; import { FirstFactorPath } from "@services/Api"; import { performAttestationCeremony } from "@services/Webauthn"; -import { extractIdentityToken } from "@utils/IdentityToken"; const RegisterWebauthn = function () { const styles = useStyles(); const navigate = useNavigate(); - const location = useLocation(); const { createErrorNotification } = useNotifications(); const [, setRegistrationInProgress] = useState(false); - const processToken = extractIdentityToken(location.search); + const processToken = useQueryParam(IdentityToken); const handleBackClick = () => { navigate(FirstFactorPath); diff --git a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx index f6ed2a055..54befc76c 100644 --- a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx +++ b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx @@ -9,9 +9,9 @@ import { useNavigate } from "react-router-dom"; import FixedTextField from "@components/FixedTextField"; import { ResetPasswordStep1Route } from "@constants/Routes"; +import { RedirectionURL, RequestMethod } from "@constants/SearchParams"; import { useNotifications } from "@hooks/NotificationsContext"; -import { useRedirectionURL } from "@hooks/RedirectionURL"; -import { useRequestMethod } from "@hooks/RequestMethod"; +import { useQueryParam } from "@hooks/QueryParam"; import { useWorkflow } from "@hooks/Workflow"; import LoginLayout from "@layouts/LoginLayout"; import { postFirstFactor } from "@services/FirstFactor"; @@ -32,8 +32,8 @@ export interface Props { const FirstFactorForm = function (props: Props) { const styles = useStyles(); const navigate = useNavigate(); - const redirectionURL = useRedirectionURL(); - const requestMethod = useRequestMethod(); + const redirectionURL = useQueryParam(RedirectionURL); + const requestMethod = useQueryParam(RequestMethod); const [workflow] = useWorkflow(); const loginChannel = useMemo(() => new BroadcastChannel("login"), []); diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx index bcb66c86b..c31d0f7c9 100644 --- a/web/src/views/LoginPortal/LoginPortal.tsx +++ b/web/src/views/LoginPortal/LoginPortal.tsx @@ -10,9 +10,10 @@ import { SecondFactorTOTPSubRoute, SecondFactorWebauthnSubRoute, } from "@constants/Routes"; +import { RedirectionURL } from "@constants/SearchParams"; import { useConfiguration } from "@hooks/Configuration"; import { useNotifications } from "@hooks/NotificationsContext"; -import { useRedirectionURL } from "@hooks/RedirectionURL"; +import { useQueryParam } from "@hooks/QueryParam"; import { useRedirector } from "@hooks/Redirector"; import { useAutheliaState } from "@hooks/State"; import { useUserInfoPOST } from "@hooks/UserInfo"; @@ -38,7 +39,7 @@ const RedirectionErrorMessage = const LoginPortal = function (props: Props) { const navigate = useNavigate(); const location = useLocation(); - const redirectionURL = useRedirectionURL(); + const redirectionURL = useQueryParam(RedirectionURL); const { createErrorNotification } = useNotifications(); const [firstFactorDisabled, setFirstFactorDisabled] = useState(true); const [broadcastRedirect, setBroadcastRedirect] = useState(false); diff --git a/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx b/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx index 5cd60d08f..268b2115f 100644 --- a/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx +++ b/web/src/views/LoginPortal/SecondFactor/OneTimePasswordMethod.tsx @@ -2,7 +2,8 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useRedirectionURL } from "@hooks/RedirectionURL"; +import { RedirectionURL } from "@constants/SearchParams"; +import { useQueryParam } from "@hooks/QueryParam"; import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration"; import { useWorkflow } from "@hooks/Workflow"; import { completeTOTPSignIn } from "@services/OneTimePassword"; @@ -33,7 +34,7 @@ const OneTimePasswordMethod = function (props: Props) { const [state, setState] = useState( props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle, ); - const redirectionURL = useRedirectionURL(); + const redirectionURL = useQueryParam(RedirectionURL); const [workflow, workflowID] = useWorkflow(); const { t: translate } = useTranslation(); diff --git a/web/src/views/LoginPortal/SecondFactor/PushNotificationMethod.tsx b/web/src/views/LoginPortal/SecondFactor/PushNotificationMethod.tsx index ea4b93890..6c37077fa 100644 --- a/web/src/views/LoginPortal/SecondFactor/PushNotificationMethod.tsx +++ b/web/src/views/LoginPortal/SecondFactor/PushNotificationMethod.tsx @@ -6,8 +6,9 @@ import makeStyles from "@mui/styles/makeStyles"; import FailureIcon from "@components/FailureIcon"; import PushNotificationIcon from "@components/PushNotificationIcon"; import SuccessIcon from "@components/SuccessIcon"; +import { RedirectionURL } from "@constants/SearchParams"; import { useIsMountedRef } from "@hooks/Mounted"; -import { useRedirectionURL } from "@hooks/RedirectionURL"; +import { useQueryParam } from "@hooks/QueryParam"; import { useWorkflow } from "@hooks/Workflow"; import { DuoDevicePostRequest, @@ -44,7 +45,7 @@ export interface Props { const PushNotificationMethod = function (props: Props) { const styles = useStyles(); const [state, setState] = useState(State.SignInInProgress); - const redirectionURL = useRedirectionURL(); + const redirectionURL = useQueryParam(RedirectionURL); const [workflow, workflowID] = useWorkflow(); const mounted = useIsMountedRef(); const [enroll_url, setEnrollUrl] = useState(""); diff --git a/web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx b/web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx index 96fa00fcf..6cd064e5e 100644 --- a/web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx +++ b/web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx @@ -6,8 +6,9 @@ import makeStyles from "@mui/styles/makeStyles"; import FailureIcon from "@components/FailureIcon"; import FingerTouchIcon from "@components/FingerTouchIcon"; import LinearProgressBar from "@components/LinearProgressBar"; +import { RedirectionURL } from "@constants/SearchParams"; import { useIsMountedRef } from "@hooks/Mounted"; -import { useRedirectionURL } from "@hooks/RedirectionURL"; +import { useQueryParam } from "@hooks/QueryParam"; import { useTimer } from "@hooks/Timer"; import { useWorkflow } from "@hooks/Workflow"; import { AssertionResult } from "@models/Webauthn"; @@ -40,7 +41,7 @@ const WebauthnMethod = function (props: Props) { const signInTimeout = 30; const [state, setState] = useState(State.WaitTouch); const styles = useStyles(); - const redirectionURL = useRedirectionURL(); + const redirectionURL = useQueryParam(RedirectionURL); const [workflow, workflowID] = useWorkflow(); const mounted = useIsMountedRef(); const [timerPercent, triggerTimer] = useTimer(signInTimeout * 1000 - 500); diff --git a/web/src/views/LoginPortal/SignOut/SignOut.tsx b/web/src/views/LoginPortal/SignOut/SignOut.tsx index 204b69a7e..917471f5d 100644 --- a/web/src/views/LoginPortal/SignOut/SignOut.tsx +++ b/web/src/views/LoginPortal/SignOut/SignOut.tsx @@ -6,9 +6,10 @@ import { useTranslation } from "react-i18next"; import { Navigate } from "react-router-dom"; import { IndexRoute } from "@constants/Routes"; +import { RedirectionURL } from "@constants/SearchParams"; import { useIsMountedRef } from "@hooks/Mounted"; import { useNotifications } from "@hooks/NotificationsContext"; -import { useRedirectionURL } from "@hooks/RedirectionURL"; +import { useQueryParam } from "@hooks/QueryParam"; import { useRedirector } from "@hooks/Redirector"; import LoginLayout from "@layouts/LoginLayout"; import { signOut } from "@services/SignOut"; @@ -19,7 +20,7 @@ const SignOut = function (props: Props) { const mounted = useIsMountedRef(); const styles = useStyles(); const { createErrorNotification } = useNotifications(); - const redirectionURL = useRedirectionURL(); + const redirectionURL = useQueryParam(RedirectionURL); const redirector = useRedirector(); const [timedOut, setTimedOut] = useState(false); const [safeRedirect, setSafeRedirect] = useState(false); diff --git a/web/src/views/ResetPassword/ResetPasswordStep2.tsx b/web/src/views/ResetPassword/ResetPasswordStep2.tsx index 0319c502a..ce3669c98 100644 --- a/web/src/views/ResetPassword/ResetPasswordStep2.tsx +++ b/web/src/views/ResetPassword/ResetPasswordStep2.tsx @@ -5,21 +5,21 @@ import { Button, Grid, IconButton, InputAdornment, Theme } from "@mui/material"; import makeStyles from "@mui/styles/makeStyles"; import classnames from "classnames"; import { useTranslation } from "react-i18next"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import FixedTextField from "@components/FixedTextField"; import PasswordMeter from "@components/PasswordMeter"; import { IndexRoute } from "@constants/Routes"; +import { IdentityToken } from "@constants/SearchParams"; import { useNotifications } from "@hooks/NotificationsContext"; +import { useQueryParam } from "@hooks/QueryParam"; import LoginLayout from "@layouts/LoginLayout"; import { PasswordPolicyConfiguration, PasswordPolicyMode } from "@models/PasswordPolicy"; import { getPasswordPolicyConfiguration } from "@services/PasswordPolicyConfiguration"; import { completeResetPasswordProcess, resetPassword } from "@services/ResetPassword"; -import { extractIdentityToken } from "@utils/IdentityToken"; const ResetPasswordStep2 = function () { const styles = useStyles(); - const location = useLocation(); const [formDisabled, setFormDisabled] = useState(true); const [password1, setPassword1] = useState(""); const [password2, setPassword2] = useState(""); @@ -43,7 +43,7 @@ const ResetPasswordStep2 = function () { // Get the token from the query param to give it back to the API when requesting // the secret for OTP. - const processToken = extractIdentityToken(location.search); + const processToken = useQueryParam(IdentityToken); const completeProcess = useCallback(async () => { if (!processToken) {