diff --git a/frontend/src/lib/hooks/redirect-uri.ts b/frontend/src/lib/hooks/redirect-uri.ts
new file mode 100644
index 0000000..bfd5d28
--- /dev/null
+++ b/frontend/src/lib/hooks/redirect-uri.ts
@@ -0,0 +1,64 @@
+type IuseRedirectUri = {
+ url?: URL;
+ valid: boolean;
+ trusted: boolean;
+ allowedProto: boolean;
+ httpsDowngrade: boolean;
+};
+
+export const useRedirectUri = (
+ redirect_uri: string | null,
+ cookieDomain: string,
+): IuseRedirectUri => {
+ let isValid = false;
+ let isTrusted = false;
+ let isAllowedProto = false;
+ let isHttpsDowngrade = false;
+
+ if (!redirect_uri) {
+ return {
+ valid: false,
+ trusted: false,
+ allowedProto: false,
+ httpsDowngrade: false,
+ };
+ }
+
+ let url: URL;
+
+ try {
+ url = new URL(redirect_uri);
+ } catch {
+ return {
+ valid: false,
+ trusted: false,
+ allowedProto: false,
+ httpsDowngrade: false,
+ };
+ }
+
+ isValid = true;
+
+ if (
+ url.hostname == cookieDomain ||
+ url.hostname.endsWith(`.${cookieDomain}`)
+ ) {
+ isTrusted = true;
+ }
+
+ if (url.protocol == "http:" || url.protocol == "https:") {
+ isAllowedProto = true;
+ }
+
+ if (window.location.protocol == "https:" && url.protocol == "http:") {
+ isHttpsDowngrade = true;
+ }
+
+ return {
+ url,
+ valid: isValid,
+ trusted: isTrusted,
+ allowedProto: isAllowedProto,
+ httpsDowngrade: isHttpsDowngrade,
+ };
+};
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
index a451ae6..7e685ac 100644
--- a/frontend/src/lib/utils.ts
+++ b/frontend/src/lib/utils.ts
@@ -5,15 +5,6 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-export const isValidUrl = (url: string) => {
- try {
- new URL(url);
- return true;
- } catch {
- return false;
- }
-};
-
export const capitalize = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
diff --git a/frontend/src/pages/continue-page.tsx b/frontend/src/pages/continue-page.tsx
index 0505428..efcf220 100644
--- a/frontend/src/pages/continue-page.tsx
+++ b/frontend/src/pages/continue-page.tsx
@@ -8,10 +8,10 @@ import {
} from "@/components/ui/card";
import { useAppContext } from "@/context/app-context";
import { useUserContext } from "@/context/user-context";
-import { isValidUrl } from "@/lib/utils";
import { Trans, useTranslation } from "react-i18next";
import { Navigate, useLocation, useNavigate } from "react-router";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { useRedirectUri } from "@/lib/hooks/redirect-uri";
export const ContinuePage = () => {
const { cookieDomain, disableUiWarnings } = useAppContext();
@@ -20,48 +20,35 @@ export const ContinuePage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
- const [loading, setLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
const [showRedirectButton, setShowRedirectButton] = useState(false);
+ const hasRedirected = useRef(false);
const searchParams = new URLSearchParams(search);
const redirectUri = searchParams.get("redirect_uri");
- const isValidRedirectUri =
- redirectUri !== null ? isValidUrl(redirectUri) : false;
- const redirectUriObj = isValidRedirectUri
- ? new URL(redirectUri as string)
- : null;
- const isTrustedRedirectUri =
- redirectUriObj !== null
- ? redirectUriObj.hostname === cookieDomain ||
- redirectUriObj.hostname.endsWith(`.${cookieDomain}`)
- : false;
- const isAllowedRedirectProto =
- redirectUriObj !== null
- ? redirectUriObj.protocol === "https:" ||
- redirectUriObj.protocol === "http:"
- : false;
- const isHttpsDowngrade =
- redirectUriObj !== null
- ? redirectUriObj.protocol === "http:" &&
- window.location.protocol === "https:"
- : false;
+ const { url, valid, trusted, allowedProto, httpsDowngrade } = useRedirectUri(
+ redirectUri,
+ cookieDomain,
+ );
- const handleRedirect = () => {
- setLoading(true);
- window.location.assign(redirectUriObj!.toString());
- };
+ const handleRedirect = useCallback(() => {
+ hasRedirected.current = true;
+ setIsLoading(true);
+ window.location.assign(url!);
+ }, [url]);
useEffect(() => {
if (!isLoggedIn) {
return;
}
+ if (hasRedirected.current) {
+ return;
+ }
+
if (
- (!isValidRedirectUri ||
- !isAllowedRedirectProto ||
- !isTrustedRedirectUri ||
- isHttpsDowngrade) &&
+ (!valid || !allowedProto || !trusted || httpsDowngrade) &&
!disableUiWarnings
) {
return;
@@ -72,7 +59,7 @@ export const ContinuePage = () => {
}, 100);
const reveal = setTimeout(() => {
- setLoading(false);
+ setIsLoading(false);
setShowRedirectButton(true);
}, 5000);
@@ -80,22 +67,33 @@ export const ContinuePage = () => {
clearTimeout(auto);
clearTimeout(reveal);
};
- });
+ }, [
+ isLoggedIn,
+ hasRedirected,
+ valid,
+ allowedProto,
+ trusted,
+ httpsDowngrade,
+ disableUiWarnings,
+ setIsLoading,
+ handleRedirect,
+ setShowRedirectButton,
+ ]);
if (!isLoggedIn) {
return (
);
}
- if (!isValidRedirectUri || !isAllowedRedirectProto) {
+ if (!valid || !allowedProto) {
return ;
}
- if (!isTrustedRedirectUri && !disableUiWarnings) {
+ if (!trusted && !disableUiWarnings) {
return (
@@ -115,8 +113,8 @@ export const ContinuePage = () => {