Skip to content

Commit 840ceb8

Browse files
authored
feat(account-center): support redirect URL parameter after successful update (#8122)
Add support for `redirect` URL parameter that allows users to be redirected to a custom URL after successfully updating their account settings. - Parse and store redirect URL from query parameter on app load - Validate URL is a valid http/https URL to prevent open redirects - On success page, redirect to stored URL if present, otherwise show success page
1 parent b7966d5 commit 840ceb8

File tree

2 files changed

+86
-5
lines changed

2 files changed

+86
-5
lines changed

packages/account-center/src/pages/UpdateSuccess/index.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { SignInIdentifier } from '@logto/schemas';
22
import type { TFuncKey } from 'i18next';
3-
import { useMemo } from 'react';
3+
import { useEffect, useMemo, useState } from 'react';
44

55
import successIllustration from '@ac/assets/icons/success.svg';
66
import ErrorPage from '@ac/components/ErrorPage';
7+
import { clearRedirectUrl, getRedirectUrl } from '@ac/utils/account-center-route';
78

89
type IdentifierType =
910
| SignInIdentifier
@@ -68,6 +69,8 @@ type Props = {
6869
};
6970

7071
const UpdateSuccess = ({ identifierType }: Props) => {
72+
const [isRedirecting, setIsRedirecting] = useState(false);
73+
7174
const translationKeys = useMemo(() => {
7275
if (!identifierType) {
7376
return translationMap.default;
@@ -76,6 +79,21 @@ const UpdateSuccess = ({ identifierType }: Props) => {
7679
return translationMap[identifierType] ?? translationMap.default;
7780
}, [identifierType]);
7881

82+
useEffect(() => {
83+
const redirectUrl = getRedirectUrl();
84+
85+
if (redirectUrl) {
86+
setIsRedirecting(true);
87+
clearRedirectUrl();
88+
window.location.assign(redirectUrl);
89+
}
90+
}, []);
91+
92+
// Show nothing while redirecting to avoid flash of success page
93+
if (isRedirecting) {
94+
return null;
95+
}
96+
7997
return (
8098
<ErrorPage
8199
illustration={successIllustration}

packages/account-center/src/utils/account-center-route.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
} from '@ac/constants/routes';
1111

1212
export const accountCenterBasePath = '/account';
13-
const storageKey = 'account-center-route-cache';
13+
const routeStorageKey = 'account-center-route-cache';
14+
const redirectStorageKey = 'logto:account-center:redirect-url';
15+
const redirectUrlParameter = 'redirect';
1416

1517
const knownRoutePrefixes: readonly string[] = [
1618
emailRoute,
@@ -41,25 +43,86 @@ const shouldSkipHandling = (search: string) => {
4143
return parameters.has('code') || parameters.has('error');
4244
};
4345

46+
/**
47+
* Get the stored redirect URL from sessionStorage.
48+
*/
49+
export const getRedirectUrl = (): string | undefined => {
50+
if (typeof window === 'undefined') {
51+
return;
52+
}
53+
54+
return sessionStorage.getItem(redirectStorageKey) ?? undefined;
55+
};
56+
57+
/**
58+
* Store the redirect URL to sessionStorage.
59+
* The URL is validated to be a valid absolute URL with http/https protocol.
60+
*/
61+
export const setRedirectUrl = (url: string): boolean => {
62+
if (typeof window === 'undefined') {
63+
return false;
64+
}
65+
66+
try {
67+
const parsed = new URL(url);
68+
// Only allow http and https protocols
69+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
70+
return false;
71+
}
72+
sessionStorage.setItem(redirectStorageKey, url);
73+
return true;
74+
} catch {
75+
// Invalid URL
76+
return false;
77+
}
78+
};
79+
80+
/**
81+
* Clear the stored redirect URL from sessionStorage.
82+
*/
83+
export const clearRedirectUrl = (): void => {
84+
if (typeof window === 'undefined') {
85+
return;
86+
}
87+
88+
sessionStorage.removeItem(redirectStorageKey);
89+
};
90+
91+
/**
92+
* Parse and store the redirect URL from the query parameter.
93+
* This needs to be done before OAuth flow starts so it persists through the sign-in.
94+
*/
95+
const handleRedirectParameter = () => {
96+
const parameters = new URLSearchParams(window.location.search);
97+
const redirectUrl = parameters.get(redirectUrlParameter);
98+
99+
if (redirectUrl) {
100+
setRedirectUrl(redirectUrl);
101+
}
102+
};
103+
44104
/**
45105
* Handle Account Center route restoration for sign in redirect.
46106
*/
47107
export const handleAccountCenterRoute = () => {
108+
// Parse and store redirect URL first (before any OAuth redirects)
109+
handleRedirectParameter();
110+
48111
if (shouldSkipHandling(window.location.search)) {
49112
return;
50113
}
51114

52115
// Restore the stored route if the current path is the base path.
53116
if (window.location.pathname === accountCenterBasePath) {
54-
const storedRoute = parseStoredRoute(sessionStorage.getItem(storageKey) ?? undefined);
117+
const storedRoute = parseStoredRoute(sessionStorage.getItem(routeStorageKey) ?? undefined);
55118
if (!storedRoute) {
56-
sessionStorage.removeItem(storageKey);
119+
sessionStorage.removeItem(routeStorageKey);
57120
return;
58121
}
59122

60123
const { search, hash } = window.location;
61124
window.history.replaceState({}, '', `${storedRoute}${search}${hash}`);
62125
} else if (isKnownRoute(window.location.pathname)) {
63-
sessionStorage.setItem(storageKey, window.location.pathname);
126+
sessionStorage.setItem(routeStorageKey, window.location.pathname);
64127
}
65128
};

0 commit comments

Comments
 (0)