diff --git a/frontend/src/contexts/userContext.ts b/frontend/src/contexts/userContext.ts index c8918030b0ae3a40cb2f3b699a9f68374fce2686..d5f18fc3de166d610def56c7cf169719830eb3c4 100644 --- a/frontend/src/contexts/userContext.ts +++ b/frontend/src/contexts/userContext.ts @@ -25,6 +25,7 @@ export const UserContext = createContext<IUserContext>({ passport: '', roles: [], consents: [], + registration_completed_date: null, }, fetchUserInfo: noop, clearUserInfo: noop, diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts index 57eaf997e95df24d2fd7018db64bc5e6a8838814..06b9f45d65e7a1e7aa5ac1cbe32449e4b1622beb 100644 --- a/frontend/src/interfaces/index.ts +++ b/frontend/src/interfaces/index.ts @@ -106,4 +106,5 @@ export interface User { passport: string roles: FetchedRole[] consents: FetchedConsent[] + registration_completed_date: Date | null } diff --git a/frontend/src/providers/userProvider.tsx b/frontend/src/providers/userProvider.tsx index f63000911c476ab03806871d1a5b66518b36b77e..041b17f78d7f0f618d14f1ca2df2bf3aa43c9c04 100644 --- a/frontend/src/providers/userProvider.tsx +++ b/frontend/src/providers/userProvider.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useState } from 'react' import { UserContext } from 'contexts' +import { parseISO } from 'date-fns' +import { User } from 'interfaces' type UserProviderProps = { children: React.ReactNode @@ -8,7 +10,7 @@ type UserProviderProps = { function UserProvider(props: UserProviderProps) { const { children } = props - const [user, setUser] = useState({ + const [user, setUser] = useState<User>({ auth: false, auth_type: '', fetched: false, @@ -23,6 +25,7 @@ function UserProvider(props: UserProviderProps) { passport: '', roles: [], consents: [], + registration_completed_date: null, }) const getUserInfo = async () => { @@ -46,6 +49,9 @@ function UserProvider(props: UserProviderProps) { passport: data.passport, roles: data.roles, consents: data.consents, + registration_completed_date: data.registration_completed_date + ? parseISO(data.registration_completed_date) + : null, }) } else { setUser({ @@ -63,6 +69,7 @@ function UserProvider(props: UserProviderProps) { passport: '', roles: [], consents: [], + registration_completed_date: null, }) } } catch (error) { @@ -81,6 +88,7 @@ function UserProvider(props: UserProviderProps) { passport: '', roles: [], consents: [], + registration_completed_date: null, }) } } @@ -113,6 +121,7 @@ function UserProvider(props: UserProviderProps) { passport: '', roles: [], consents: [], + registration_completed_date: null, }) } diff --git a/frontend/src/routes/landing/index.tsx b/frontend/src/routes/landing/index.tsx index 97fbd4110a221d4d90814b947cb4d063e99a3a07..12702a8053ec190c28696620cbee26075864a9db 100644 --- a/frontend/src/routes/landing/index.tsx +++ b/frontend/src/routes/landing/index.tsx @@ -123,9 +123,12 @@ function LandingPage() { const { user } = useUserContext() if (user.sponsor_id) { - return <Redirect to="sponsor" /> + return <Redirect to="/sponsor" /> } if (user.person_id) { + if (!user.registration_completed_date) { + return <Redirect to="/guestregister" /> + } return <GuestPage /> } return <AnonPage /> diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index eda7fa6244364a24e72b32a4b16d9ef65fc470bb..e35626b11823d8ea2345220cf3c41fd9975b527c 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -7,6 +7,7 @@ from django.db import transaction from django.http.response import JsonResponse from django.utils import timezone from django.db.models import Q +from django.contrib.auth.models import AnonymousUser from rest_framework import status from rest_framework.authentication import SessionAuthentication, BasicAuthentication from rest_framework.generics import CreateAPIView, GenericAPIView, DestroyAPIView @@ -214,10 +215,30 @@ class InvitedGuestView(GenericAPIView): in the frontend, before calling the post endpoint defined below with updated info and confirmation. """ + + # Find the invitation related to the invite invite_id = request.session.get("invite_id") - try: - invite_link = InvitationLink.objects.get(uuid=invite_id) - except (InvitationLink.DoesNotExist, exceptions.ValidationError): + invite_link = None + if invite_id: + # The proper way using an invite link with id set in session + try: + invite_link = InvitationLink.objects.get(uuid=invite_id) + except (InvitationLink.DoesNotExist, exceptions.ValidationError): + pass + else: + # Last ditch effort to find an invite link through the user object if the + # user has logged in, connecting the invite to a user previously. + if not isinstance(request.user, AnonymousUser): + try: + greguser = GregUserProfile.objects.get(user=request.user) + except GregUserProfile.DoesNotExist: + greguser = None + if greguser and greguser.person: + invite_link = InvitationLink.objects.filter( + invitation__role__person=greguser.person + ).first() + request.session["invite_id"] = str(invite_link.uuid) + if not invite_link: return Response( status=status.HTTP_403_FORBIDDEN, data={"code": "invalid_invite", "message": "Invalid invite"}, @@ -228,8 +249,7 @@ class InvitedGuestView(GenericAPIView): data={"code": "invite_expired", "message": "Invite expired"}, ) - # if invite_id: - invite_link = InvitationLink.objects.get(uuid=invite_id) + # An invitation was found. Construct response and send it role = invite_link.invitation.role person = role.person sponsor = role.sponsor diff --git a/gregui/api/views/userinfo.py b/gregui/api/views/userinfo.py index 2eaeb68678849b9e7ce647613d20c93f60c3fa69..d07f249f08b1209c496baf9da234f97f9cec81a1 100644 --- a/gregui/api/views/userinfo.py +++ b/gregui/api/views/userinfo.py @@ -45,6 +45,7 @@ class UserInfoView(APIView): "roles": [], "consents": [], "auth_type": auth_type, + "registration_completed_date": None, } if user.is_authenticated: @@ -127,6 +128,7 @@ class UserInfoView(APIView): "passport": person.passport and person.passport.value, "roles": person_roles, "consents": person_consents, + "registration_completed_date": person.registration_completed_date, } ) return Response(content) diff --git a/gregui/tests/api/views/test_invitation.py b/gregui/tests/api/views/test_invitation.py index 152c1b367e55bc071122454ad3f9e4a4e72e2830..b1efd36b658a8a8da0da40c8ab0597ab572f9d3b 100644 --- a/gregui/tests/api/views/test_invitation.py +++ b/gregui/tests/api/views/test_invitation.py @@ -110,6 +110,52 @@ def test_get_invited_info_session_okay( ) +@pytest.mark.django_db +def test_get_invited_info_okay_user_connected_before( + client, + user_invited_person, + invited_person, + log_in, + unit_foo, + role_type_foo, + sponsor_foo_data, +): + """ + If a logged in user has a profile connected to a person, their + invite is found and they are given access to continue registering. + """ + log_in(user_invited_person) + pe, _ = invited_person + response = client.get(reverse("gregui-v1:invited-info")) + + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert data.get("person") == dict( + first_name=pe.first_name, + last_name=pe.last_name, + private_email=pe.private_email.value, + private_mobile=None, + fnr=None, + passport=None, + feide_id=None, + date_of_birth=None, + gender=None, + ) + assert data.get("sponsor") == dict( + first_name=sponsor_foo_data["first_name"], + last_name=sponsor_foo_data["last_name"], + ) + assert data.get("role") == dict( + start=None, + end="2050-10-15", + contact_person_unit="", + ou_name_en=unit_foo.name_en, + ou_name_nb=unit_foo.name_nb, + role_name_en=role_type_foo.name_en, + role_name_nb=role_type_foo.name_nb, + ) + + @pytest.mark.django_db def test_get_invited_info_expired_link( client, invitation_link, invitation_expired_date @@ -128,6 +174,18 @@ def test_get_invited_info_expired_link( assert response.status_code == status.HTTP_403_FORBIDDEN +@pytest.mark.django_db +def test_get_invited_info_logged_in_user(client, log_in, user_no_profile): + """ + Logged in users with no invite_id in session and no profile + connected to user are forbidden. + """ + log_in(user_no_profile) + response = client.get(reverse("gregui-v1:invited-info")) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.content == b'{"code":"invalid_invite","message":"Invalid invite"}' + + @pytest.mark.django_db def test_invited_guest_can_post_information( client: APIClient, invited_person, confirmation_template diff --git a/gregui/tests/api/views/test_userinfo.py b/gregui/tests/api/views/test_userinfo.py index ff519513129d4ed76fd3d3da96281031eb0d7ae1..b135ff8e0ba8fde0d89f480f66fd3adda9808b47 100644 --- a/gregui/tests/api/views/test_userinfo.py +++ b/gregui/tests/api/views/test_userinfo.py @@ -29,6 +29,7 @@ def test_userinfo_invited_get(client, invitation_link): "mobile_phone": None, "fnr": None, "passport": None, + "registration_completed_date": None, "roles": [ { "id": 1, @@ -67,6 +68,7 @@ def test_userinfo_sponsor_get(client, log_in, user_sponsor): "roles": [], "consents": [], "sponsor_id": 1, + "registration_completed_date": None, } @@ -89,4 +91,5 @@ def test_userinfo_guest_get(client, log_in, user_person): "mobile_phone": None, "fnr": "123456*****", "passport": None, + "registration_completed_date": None, } diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py index 2ce271496c8bef763cbce72eee760b66700c3e32..e3571037b38bd6796db4163874a31871e9558d92 100644 --- a/gregui/tests/conftest.py +++ b/gregui/tests/conftest.py @@ -423,6 +423,24 @@ def invited_person( ) +@pytest.fixture +def user_invited_person(invited_person, create_user) -> User: + user_model = get_user_model() + pe, _ = invited_person + + # Create a user and link him to a sponsor + user = create_user( + username="test_person", + email="person@example.org", + first_name="Test", + last_name="Person", + ) + GregUserProfile.objects.create(user=user, person=pe) + + # This user is a sponsor for unit_foo + return user_model.objects.get(id=user.id) + + @pytest.fixture def invited_person_no_ids( create_person,