Skip to content

Commit

Permalink
feat: add redirection to previous page after login (#560)
Browse files Browse the repository at this point in the history
* feat: redirection after login

* feat: redirection after login with otp

* feat: redirect on oauth
  • Loading branch information
sdrejkarz authored May 22, 2024
1 parent 04c6757 commit 423afdc
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useCallback } from 'react';
import { apiURL } from '../helpers';
import { OAuthProvider } from './auth.types';

export const getOauthUrl = (provider: OAuthProvider, locale?: string) =>
apiURL(`/auth/social/login/${provider}?next=${encodeURIComponent(window.location.origin)}&locale=${locale ?? 'en'}`);
export const getOauthUrl = (provider: OAuthProvider, locale = 'en') =>
apiURL(`/auth/social/login/${provider}?next=${encodeURIComponent(window.location.href)}&locale=${locale}`);

export const useOAuthLogin = () => {
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { authSinginMutation } from '../loginForm.graphql';

jest.mock('@sb/webapp-core/services/analytics');

const mockSearch = '?redirect=%2Fen%2Fprofile';
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => {
return {
...jest.requireActual<NodeModule>('react-router-dom'),
useNavigate: () => mockNavigate,
useLocation: () => ({ search: mockSearch }),
};
});
const Component = () => <LoginForm />;
Expand All @@ -34,14 +36,42 @@ describe('LoginForm: Component', () => {

const getEmailInput = async () => await screen.findByLabelText(/email/i);
const getPasswordInput = async () => await screen.findByLabelText(/password/i);
const clickLoginButton = async () => userEvent.click(await screen.findByRole('button', { name: /log in/i }));
const clickLoginButton = async () => await userEvent.click(await screen.findByRole('button', { name: /log in/i }));
const user = currentUserFactory({
firstName: 'Jack',
lastName: 'White',
email: '[email protected]',

roles: [Role.USER],
});

it('should redirect with searchParams if otp available', async () => {
const refreshQueryMock = fillCommonQueryWithUser(user);
const requestMock = composeMockedQueryResult(authSinginMutation, {
variables: mockCredentials,
data: {
tokenAuth: {
access: 'access-token',
refresh: 'refresh-token',
otpAuthToken: 'otpAuthToken',
},
},
});

const { waitForApolloMocks } = render(<Component />, {
apolloMocks: [requestMock, refreshQueryMock],
});
await waitForApolloMocks();

await userEvent.type(await getEmailInput(), mockCredentials.input.email);
await userEvent.type(await getPasswordInput(), mockCredentials.input.password);

await clickLoginButton();

expect(trackEvent).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith({ pathname: '/en/auth/validate-otp', search: mockSearch });
});

it('should call login action when submitted', async () => {
const refreshQueryMock = fillCommonQueryWithUser(user);
const requestMock = composeMockedQueryResult(authSinginMutation, {
Expand All @@ -65,7 +95,6 @@ describe('LoginForm: Component', () => {
await waitForApolloMocks();

expect(trackEvent).toHaveBeenCalledWith('auth', 'log-in');
expect(await mockNavigate).toHaveBeenCalledWith(`/en`);
});

it('should show error if required value is missing', async () => {
Expand All @@ -76,7 +105,6 @@ describe('LoginForm: Component', () => {
await clickLoginButton();

expect(await screen.findByText('Password is required')).toBeInTheDocument();
expect(await mockNavigate).not.toHaveBeenCalled();
});

it('should show generic form error if action throws error', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useCommonQuery } from '@sb/webapp-api-client/providers';
import { useGenerateLocalePath } from '@sb/webapp-core/hooks';
import { trackEvent } from '@sb/webapp-core/services/analytics';
import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';

import { RoutesConfig } from '../../../../app/config/routes';
import { authSinginMutation } from './loginForm.graphql';
Expand All @@ -16,6 +16,7 @@ export const useLoginForm = () => {
const navigate = useNavigate();
const generateLocalePath = useGenerateLocalePath();
const { reload: reloadCommonQuery } = useCommonQuery();
const { search } = useLocation();

const form = useApiForm<LoginFormFields>({
errorMessages: {
Expand All @@ -36,14 +37,15 @@ export const useLoginForm = () => {
const [commitLoginMutation, { loading }] = useMutation(authSinginMutation, {
onCompleted: ({ tokenAuth }) => {
if (tokenAuth?.otpAuthToken) {
return navigate(generateLocalePath(RoutesConfig.validateOtp));
return navigate({
pathname: generateLocalePath(RoutesConfig.validateOtp),
search: search || undefined,
});
}

reloadCommonQuery();

trackEvent('auth', 'log-in');

navigate(generateLocalePath(RoutesConfig.home));
},
onError: (error) => {
setApolloGraphQLResponseErrors(error.graphQLErrors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('ValidateOtpForm: Component', () => {
mockNavigate.mockReset();
});

it('should redirect after successful validation', async () => {
it('should call trackEvent after successful validation', async () => {
const token = '331553';
const requestMock = composeMockedQueryResult(validateOtpMutation, {
variables: { input: { otpToken: token } },
Expand All @@ -51,7 +51,6 @@ describe('ValidateOtpForm: Component', () => {
await userEvent.click(submitButton);
await waitForApolloMocks();

expect(mockNavigate).toHaveBeenCalledWith(`/en`);
expect(trackEvent).toHaveBeenCalledWith('auth', 'otp-validate');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import { useCommonQuery } from '@sb/webapp-api-client/providers';
import { Button, ButtonSize } from '@sb/webapp-core/components/buttons';
import { Input } from '@sb/webapp-core/components/forms';
import { H3, Small } from '@sb/webapp-core/components/typography';
import { useGenerateLocalePath } from '@sb/webapp-core/hooks';
import { trackEvent } from '@sb/webapp-core/services/analytics';
import { FormattedMessage, useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';

import { RoutesConfig } from '../../../../app/config/routes';
import { validateOtpMutation } from '../twoFactorAuthForm/twoFactorAuthForm.graphql';

export type ValidateOtpFormFields = {
Expand All @@ -18,9 +15,7 @@ export type ValidateOtpFormFields = {

export const ValidateOtpForm = () => {
const intl = useIntl();
const navigate = useNavigate();
const form = useApiForm<ValidateOtpFormFields>();
const generateLocalePath = useGenerateLocalePath();
const {
handleSubmit,
hasGenericErrorOnly,
Expand All @@ -43,7 +38,6 @@ export const ValidateOtpForm = () => {
const { data } = await commitValidateOtpMutation({ variables: { input: { otpToken: values.token } } });
if (data?.validateOtp?.access) {
reloadCommonQuery();
navigate(generateLocalePath(RoutesConfig.home));
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('AnonymousRoute: Component', () => {
</Route>
<Route path="/en/" element={<span />} />
<Route path="/en/home" element={<span data-testid="home-content" />} />
<Route path="/en/profile" element={<span data-testid="profile-content" />} />
</Routes>
);

Expand All @@ -39,5 +40,24 @@ describe('AnonymousRoute: Component', () => {
expect(screen.queryByTestId('content')).not.toBeInTheDocument();
expect(screen.queryByTestId('home-content')).not.toBeInTheDocument();
});

it('should redirect to redirectUrl', async () => {
const mockSearch = 'en/profile';
const spy = jest.spyOn(URLSearchParams.prototype, 'get').mockImplementation((key) => mockSearch);

const apolloMocks = [
fillCommonQueryWithUser(
currentUserFactory({
roles: [Role.ADMIN],
})
),
];

const { waitForApolloMocks } = render(<Component />, { apolloMocks });
await waitForApolloMocks(0);
expect(screen.queryByTestId('content')).not.toBeInTheDocument();
expect(screen.getByTestId('profile-content')).toBeInTheDocument();
spy.mockClear();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useGenerateLocalePath } from '@sb/webapp-core/hooks';
import { Navigate, Outlet } from 'react-router-dom';
import { Navigate, Outlet, useSearchParams } from 'react-router-dom';

import { RoutesConfig } from '../../../../app/config/routes';
import { useAuth } from '../../../hooks';
Expand All @@ -21,6 +21,8 @@ import { useAuth } from '../../../hooks';
export const AnonymousRoute = () => {
const generateLocalePath = useGenerateLocalePath();
const { isLoggedIn } = useAuth();
const [search] = useSearchParams();
const redirect = search.get('redirect');

return isLoggedIn ? <Navigate to={generateLocalePath(RoutesConfig.home)} /> : <Outlet />;
return isLoggedIn ? <Navigate to={redirect ?? generateLocalePath(RoutesConfig.home)} /> : <Outlet />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Navigate, Outlet } from 'react-router-dom';
import { RoutesConfig } from '../../../../app/config/routes';
import { Role } from '../../../../modules/auth/auth.types';
import { useAuth, useRoleAccessCheck } from '../../../hooks';
import { useGenerateRedirectSearchParams } from './authRoute.hook';

export type AuthRouteProps = {
allowedRoles?: Role | Role[];
Expand All @@ -28,9 +29,21 @@ export type AuthRouteProps = {
export const AuthRoute = ({ allowedRoles = [Role.ADMIN, Role.USER] }: AuthRouteProps) => {
const { isLoggedIn } = useAuth();
const { isAllowed } = useRoleAccessCheck(allowedRoles);

const generateRedirectSearchParams = useGenerateRedirectSearchParams();
const generateLocalePath = useGenerateLocalePath();
const fallbackUrl = isLoggedIn ? generateLocalePath(RoutesConfig.notFound) : generateLocalePath(RoutesConfig.login);

if (!isAllowed) return <Navigate to={fallbackUrl} />;
if (!isAllowed) {
if (isLoggedIn) return <Navigate to={generateLocalePath(RoutesConfig.notFound)} />;

return (
<Navigate
to={{
pathname: generateLocalePath(RoutesConfig.login),
search: generateRedirectSearchParams(),
}}
/>
);
}
return <Outlet />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useGenerateLocalePath, useLocale } from '@sb/webapp-core/hooks';
import { createSearchParams, useLocation } from 'react-router-dom';

export const useGenerateRedirectSearchParams = () => {
const generateLocalePath = useGenerateLocalePath();
const locale = useLocale();
const { pathname } = useLocation();

const generateRedirectSearchParams = () => {
const re = new RegExp(`/${locale}/?`);
const pathnameWithoutLocale = pathname.replace(re, '');
const redirect = generateLocalePath(pathnameWithoutLocale);
return pathnameWithoutLocale ? createSearchParams({ redirect }).toString() : undefined;
};

return generateRedirectSearchParams;
};

0 comments on commit 423afdc

Please sign in to comment.