Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HAI-1884 Identify user coming from invitation link #374

Merged
merged 2 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
88 changes: 88 additions & 0 deletions src/domain/auth/components/UserIdentify.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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';
import * as hankeUsersApi from '../../hanke/hankeUsers/hankeUsersApi';

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

const id = '5ArrqPT6kW97QTK7t7ya9PA2';
const path = `/fi/kutsu?id=${id}`;

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);
const identifyUser = jest.spyOn(hankeUsersApi, 'identifyUser');

getWrapper();

await waitFor(() => expect(window.document.title).toBe('Haitaton - Etusivu'));
expect(identifyUser).toHaveBeenCalledWith(id);
expect(
screen.queryByText(
'Tunnistautuminen onnistui. Sinut on nyt lisätty hankkeelle (Aidasmäentien vesihuollon rakentaminen HAI22-2).',
),
).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({ hankeNimi, hankeTunnus }) {
setNotification(true, {
label: t('hankeUsers:notifications:userIdentifiedLabel'),
message: t('hankeUsers:notifications:userIdentifiedText', { hankeNimi, hankeTunnus }),
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;
6 changes: 6 additions & 0 deletions src/domain/hanke/hankeUsers/hankeUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ export type SignedInUser = {
kayttooikeustaso: keyof typeof AccessRightLevel;
kayttooikeudet: UserRights;
};

export type IdentificationResponse = {
kayttajaId: string;
hankeTunnus: string;
hankeNimi: string;
};
7 changes: 6 additions & 1 deletion src/domain/hanke/hankeUsers/hankeUsersApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import api from '../../api/api';
import { HankeUser, SignedInUser } from './hankeUser';
import { HankeUser, IdentificationResponse, SignedInUser } from './hankeUser';

export async function getHankeUsers(hankeTunnus: string) {
const { data } = await api.get<{ kayttajat: HankeUser[] }>(`hankkeet/${hankeTunnus}/kayttajat`);
Expand All @@ -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<IdentificationResponse>('kayttajat', { tunniste: id });
return data;
}
13 changes: 12 additions & 1 deletion src/domain/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as hankkeetDB from './data/hankkeet';
import * as hakemuksetDB from './data/hakemukset';
import * as usersDB from './data/users';
import ApiError from './apiError';
import { SignedInUser } from '../hanke/hankeUsers/hankeUser';
import { IdentificationResponse, SignedInUser } from '../hanke/hankeUsers/hankeUser';

const apiUrl = '/api';

Expand Down Expand Up @@ -204,4 +204,15 @@ export const handlers = [
}),
);
}),

rest.post(`${apiUrl}/kayttajat`, async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<IdentificationResponse>({
kayttajaId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
hankeTunnus: 'HAI22-2',
hankeNimi: 'Aidasmäentien vesihuollon rakentaminen',
}),
);
}),
];
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 ({{hankeNimi}} {{hankeTunnus}}).",
"userIdentifiedError": "Tunnistautuminen epäonnistui"
}
},
"map": {
Expand Down
Loading