Skip to content

Commit

Permalink
HAI-1884 Identify user coming from invitation link
Browse files Browse the repository at this point in the history
When user comes to Haitaton from invitation link, call identify
endpoint with token included in the link. After identifying user,
show success notification and redirect user to front page.
If user is not signed in before coming to Haitaton, redirect user
to login and after login, do the identifying.
  • Loading branch information
markohaarni committed Sep 22, 2023
1 parent 780dd03 commit 9be8485
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/common/routes/LocaleRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import EditJohtoselvitysPage from '../../pages/EditJohtoselvitysPage';
import NotFoundPage from '../../pages/staticPages/404Page';
import ManualPage from '../../pages/staticPages/ManualPage';
import AccessRightsPage from '../../pages/AccessRightsPage';
import UserIdentify from '../../domain/auth/components/UserIdentify';

const LocaleRoutes = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -74,6 +75,7 @@ const LocaleRoutes = () => {
<Route path={t('routes:REFERENCES:path')} element={<ReferencesPage />} />
<Route path={t('routes:PRIVACY_POLICY:path')} element={<PrivacyPolicyPage />} />
<Route path={t('routes:MANUAL:path')} element={<ManualPage />} />
<Route path={t('routes:IDENTIFY_USER:path')} element={<UserIdentify />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
);
Expand Down
80 changes: 80 additions & 0 deletions src/domain/auth/components/UserIdentify.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import { QueryClient, QueryClientProvider } from 'react-query';
import { InitialEntry } from '@remix-run/router';
import { User } from 'oidc-client';
import { Provider } from 'react-redux';
import AppRoutes from '../../../common/routes/AppRoutes';
import { FeatureFlagsProvider } from '../../../common/components/featureFlags/FeatureFlagsContext';
import { GlobalNotificationProvider } from '../../../common/components/globalNotification/GlobalNotificationContext';
import GlobalNotification from '../../../common/components/globalNotification/GlobalNotification';
import { store } from '../../../common/redux/store';
import i18n from '../../../locales/i18n';
import authService from '../authService';
import { REDIRECT_PATH_KEY } from '../../../common/routes/constants';

afterEach(() => {
sessionStorage.clear();
});

const path = '/fi/kutsu?id=5ArrqPT6kW97QTK7t7ya9PA2';

const mockUser: Partial<User> = {
id_token: 'fffff-aaaaaa-11111',
access_token: '.BnutWVN1x7RSAP5bU2a-tXdVPuof_9pBNd_Ozw',
profile: {
iss: '',
sub: '',
aud: '',
exp: 0,
iat: 0,
},
};

function getWrapper(routerInitialEntries?: InitialEntry[]) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retryDelay: 0,
},
},
});

return render(
<MemoryRouter initialEntries={routerInitialEntries}>
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18n}>
<FeatureFlagsProvider>
<GlobalNotificationProvider>
<AppRoutes />
<GlobalNotification />
</GlobalNotificationProvider>
</FeatureFlagsProvider>
</I18nextProvider>
</QueryClientProvider>
</Provider>
</MemoryRouter>,
);
}

test('Should save path with query string to session storage and navigate to login when going to invitation route', async () => {
const login = jest.spyOn(authService, 'login').mockResolvedValue();

getWrapper([path]);

await waitFor(() => expect(login).toHaveBeenCalled());
expect(window.sessionStorage.getItem(REDIRECT_PATH_KEY)).toBe(path);
});

test('Should identify user after login', async () => {
sessionStorage.setItem(REDIRECT_PATH_KEY, path);
jest.spyOn(authService.userManager, 'getUser').mockResolvedValue(mockUser as User);

getWrapper();

await waitFor(() => expect(window.document.title).toBe('Haitaton - Etusivu'));
expect(screen.queryByText('Tunnistautuminen onnistui')).toBeInTheDocument();
});
66 changes: 66 additions & 0 deletions src/domain/auth/components/UserIdentify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { Navigate, useLocation, useSearchParams } from 'react-router-dom';
import ErrorLoadingText from '../../../common/components/errorLoadingText/ErrorLoadingText';
import { useGlobalNotification } from '../../../common/components/globalNotification/GlobalNotificationContext';
import useLocale from '../../../common/hooks/useLocale';
import { REDIRECT_PATH_KEY } from '../../../common/routes/constants';
import { identifyUser } from '../../hanke/hankeUsers/hankeUsersApi';
import useUser from '../useUser';

function UserIdentify() {
const { data: user } = useUser();
const isAuthenticated = Boolean(user?.profile);
const locale = useLocale();
const { t } = useTranslation();
const location = useLocation();
const [searchParams] = useSearchParams();
const id = searchParams.get('id');
const { setNotification } = useGlobalNotification();
const identifyUserCalled = useRef(false);

const { mutate, isSuccess, isError } = useMutation(identifyUser);

useEffect(() => {
if (!isAuthenticated) {
sessionStorage.setItem(REDIRECT_PATH_KEY, `${location.pathname}${location.search}`);
}
}, [isAuthenticated, location.pathname, location.search]);

useEffect(() => {
if (isAuthenticated && id !== null && !identifyUserCalled.current) {
mutate(id, {
onSuccess() {
setNotification(true, {
label: t('hankeUsers:notifications:userIdentifiedLabel'),
message: t('hankeUsers:notifications:userIdentifiedText'),
type: 'success',
dismissible: true,
closeButtonLabelText: t('common:components:notification:closeButtonLabelText'),
});
},
onSettled() {
identifyUserCalled.current = true;
sessionStorage.removeItem(REDIRECT_PATH_KEY);
},
});
}
}, [isAuthenticated, id, mutate, setNotification, t]);

if (isError) {
return <ErrorLoadingText>{t('hankeUsers:notifications:userIdentifiedError')}</ErrorLoadingText>;
}

if (!isAuthenticated) {
return <Navigate to="/login" />;
}

if (isSuccess) {
return <Navigate to={`/${locale}`} />;
}

return null;
}

export default UserIdentify;
5 changes: 5 additions & 0 deletions src/domain/hanke/hankeUsers/hankeUsersApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ export async function getSignedInUser(hankeTunnus?: string): Promise<SignedInUse
const { data } = await api.get<SignedInUser>(`hankkeet/${hankeTunnus}/whoami`);
return data;
}

export async function identifyUser(id: string) {
const { data } = await api.post('kayttajat', { tunniste: id });
return data;
}
4 changes: 4 additions & 0 deletions src/domain/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,8 @@ export const handlers = [
}),
);
}),

rest.post(`${apiUrl}/kayttajat`, async (req, res, ctx) => {
return res(ctx.status(204));
}),
];
12 changes: 11 additions & 1 deletion src/locales/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,13 @@
"meta": {
"title": "Haitaton - Käyttöohjeet ja tuki"
}
},
"IDENTIFY_USER": {
"path": "/kutsu",
"headerLabel": "Tunnistaudu Haitattomaan",
"meta": {
"title": "Haitaton - Tunnistaudu Haitattomaan"
}
}
},
"hanke": {
Expand Down Expand Up @@ -850,7 +857,10 @@
"rightsUpdatedSuccessLabel": "Käyttöoikeudet päivitetty",
"rightsUpdatedSuccessText": "Käyttöoikeudet on päivitetty onnistuneesti",
"rightsUpdatedErrorLabel": "Virhe päivityksessä",
"rightsUpdatedErrorText": "<0>Käyttöoikeuksien päivityksessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa <1>[email protected]</1>.</0>"
"rightsUpdatedErrorText": "<0>Käyttöoikeuksien päivityksessä tapahtui virhe. Yritä myöhemmin uudelleen tai ota yhteyttä Haitattoman tekniseen tukeen sähköpostiosoitteessa <1>[email protected]</1>.</0>",
"userIdentifiedLabel": "Tunnistautuminen onnistui",
"userIdentifiedText": "Tunnistautuminen onnistui. Sinut on nyt lisätty hankkeelle.",
"userIdentifiedError": "Tunnistautuminen epäonnistui"
}
},
"map": {
Expand Down

0 comments on commit 9be8485

Please sign in to comment.