From 6e8ba6aaee3ae5f4987304eb99853d2583ed60c3 Mon Sep 17 00:00:00 2001
From: Jonas Braathen <jonas.braathen@usit.uio.no>
Date: Fri, 10 Dec 2021 09:49:48 +0100
Subject: [PATCH] Improve invitation link handling

- Combine the /invite and /invitelink endpoints into one
- Return error messages from the backend
- Handle errors in the frontend
- Lower number of queries ran when calling the userinfo endpoint
---
 frontend/public/locales/en/common.json        |   4 +-
 frontend/public/locales/en/invite.json        |  10 +-
 frontend/public/locales/nb/common.json        |   4 +-
 frontend/public/locales/nb/invite.json        |   6 +-
 frontend/public/locales/nn/common.json        |   4 +-
 frontend/public/locales/nn/invite.json        |   6 +-
 frontend/src/routes/index.tsx                 |  15 +-
 frontend/src/routes/invite/index.tsx          | 131 +++++++++++++++++-
 .../routes/{invitelink => invite}/logout.tsx  |   0
 frontend/src/routes/invitelink/index.tsx      |  18 ---
 gregui/api/views/invitation.py                |  26 +++-
 gregui/api/views/userinfo.py                  |  77 +++++-----
 gregui/tests/api/views/test_invitation.py     |  55 ++++++--
 gregui/tests/api/views/test_userinfo.py       |  10 +-
 14 files changed, 274 insertions(+), 92 deletions(-)
 rename frontend/src/routes/{invitelink => invite}/logout.tsx (100%)
 delete mode 100644 frontend/src/routes/invitelink/index.tsx

diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json
index bf866efa..d7bf4b92 100644
--- a/frontend/public/locales/en/common.json
+++ b/frontend/public/locales/en/common.json
@@ -146,9 +146,11 @@
     "cancelInvitationDescription": "Do you want to cancel the invitation?"
   },
   "error": {
+    "error": "Error",
     "invitationCreationFailedHeader": "Failed to create invite",
     "errorStatusCode": "Status code: {{statusCode}} (<3>{{statusText}}</3>)",
     "genericServerErrorBody": "The server reported:<1>{{errorBodyText}}</1>",
-    "contactHelp": "Contact help through the link in the footer if the problem persists."
+    "contactHelp": "Contact help through the link in the footer if the problem persists.",
+    "unknown": "An unknown error has occurred. If the problem persists, contact support."
   }
 }
diff --git a/frontend/public/locales/en/invite.json b/frontend/public/locales/en/invite.json
index 70693d27..ac094ff1 100644
--- a/frontend/public/locales/en/invite.json
+++ b/frontend/public/locales/en/invite.json
@@ -1,6 +1,10 @@
 {
-  "description": "Please choose how you want to log in to complete your registration. The recommended way is to log in with either Feide or ID-porten. If that is not possible you can manually fill out the registration form with your passport number.",
-  "header": "Guest Registration",
+  "description": "Please choose how you want to log in to complete your registration. The recommended way is to log in with either Feide or ID-porten. If that is not possible you can manually fill out the registration form with your passport number or Norwegian national ID number.",
+  "header": "Guest registration",
   "login": "Log in with FEIDE",
-  "manual": "Registrate manually"
+  "manual": "Register manually",
+  "errors": {
+    "invalidToken": "The invitation link you followed is invalid.",
+    "expiredToken": "The invitation link you followed has expired. Contact your host to get a new link."
+  }
 }
diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json
index 290d8f21..f716a33c 100644
--- a/frontend/public/locales/nb/common.json
+++ b/frontend/public/locales/nb/common.json
@@ -146,9 +146,11 @@
     "cancelInvitationDescription": "Vil du kansellere invitasjonen?"
   },
   "error": {
+    "error": "Feil",
     "invitationCreationFailedHeader": "Kunne ikke opprette invitasjon",
     "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)",
     "genericServerErrorBody": "Respons fra server:<1>{{errorBodyText}}</1>",
-    "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer."
+    "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.",
+    "unknown": "En ukjent feil har oppstått. Om problemet vedvarer, kontakt brukerstøtte."
   }
 }
diff --git a/frontend/public/locales/nb/invite.json b/frontend/public/locales/nb/invite.json
index 6f6b73f0..8ad98cd8 100644
--- a/frontend/public/locales/nb/invite.json
+++ b/frontend/public/locales/nb/invite.json
@@ -2,5 +2,9 @@
   "description": "Vennligst velg hvordan du vil logge inn for å fullføre registreringen. Den anbefalte metoden er å logge inn gjennom Feide eller ID-porten. Dersom det ikke er mulig kan du fylle ut registreringskjemaet manuelt med passnummer",
   "header": "Gjestetjenesten",
   "login": "Logg inn med FEIDE",
-  "manual": "Registrer manuelt"
+  "manual": "Registrer manuelt",
+  "errors": {
+    "invalidToken": "Denne invitasjonslenka er ugyldig.",
+    "expiredToken": "Denne invitasjonslenka er utløpt. Kontakt verten din for å få tilsendt en ny."
+  }
 }
diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json
index b7beee8b..fa5e72fc 100644
--- a/frontend/public/locales/nn/common.json
+++ b/frontend/public/locales/nn/common.json
@@ -147,9 +147,11 @@
     "cancelInvitationDescription": "Vil du kansellere invitasjonen?"
   },
   "error": {
+    "error": "Feil",
     "invitationCreationFailedHeader": "Kunne ikkje opprette invitasjon",
     "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)",
     "genericServerErrorBody": "Respons frå server:<1>{{errorBodyText}}</1>",
-    "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer."
+    "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.",
+    "unknown": "Ein uventa feil oppstod. Om problemet varer ved, kontakt brukarstøtte."
   }
 }
diff --git a/frontend/public/locales/nn/invite.json b/frontend/public/locales/nn/invite.json
index abb2afaf..155b826b 100644
--- a/frontend/public/locales/nn/invite.json
+++ b/frontend/public/locales/nn/invite.json
@@ -2,5 +2,9 @@
   "description": "Ver venleg og vel korleis du vil logge inn for å fullføre registreringa. Den anbefalte metoden er å logge inn gjennom Feide eller ID-porten. Dersom det ikkje er mogeleg kan du fylle ut registreringskjemaet manuelt med passnummer",
   "header": "Gjestetjenesten",
   "login": "Logg inn med FEIDE",
-  "manual": "Registrer manuelt"
+  "manual": "Registrer manuelt",
+  "errors": {
+    "invalidToken": "Denne invitasjonslenka er ugyldig.",
+    "expiredToken": "Denne invitasjonslenka har utløpe. Kontakt verten din for å få tilsendt ei ny."
+  }
 }
diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx
index e273442a..1276166a 100644
--- a/frontend/src/routes/index.tsx
+++ b/frontend/src/routes/index.tsx
@@ -5,24 +5,24 @@ import { styled } from '@mui/system'
 import { CssBaseline } from '@mui/material'
 import fetchIntercept from 'fetch-intercept'
 
+import { registerLocale } from 'i18n-iso-countries'
+import i18n_iso_countries_en from 'i18n-iso-countries/langs/en.json'
+import i18n_iso_countries_nb from 'i18n-iso-countries/langs/nb.json'
+import i18n_iso_countries_nn from 'i18n-iso-countries/langs/nn.json'
+
 import { useUserContext } from 'contexts'
 import { getCookie, deleteCookie } from 'utils'
 
+import GuestRegister from 'routes/guest/register'
 import Sponsor from 'routes/sponsor'
 import Register from 'routes/sponsor/register'
 import FrontPage from 'routes/frontpage'
 import Invite from 'routes/invite'
-import InviteLink from 'routes/invitelink'
-import LogoutInviteSession from 'routes/invitelink/logout'
+import LogoutInviteSession from 'routes/invite/logout'
 import Footer from 'routes/components/footer'
 import Header from 'routes/components/header'
 import NotFound from 'routes/components/notFound'
 import ProtectedRoute from 'components/protectedRoute'
-import { registerLocale } from 'i18n-iso-countries'
-import i18n_iso_countries_en from 'i18n-iso-countries/langs/en.json'
-import i18n_iso_countries_nb from 'i18n-iso-countries/langs/nb.json'
-import i18n_iso_countries_nn from 'i18n-iso-countries/langs/nn.json'
-import GuestRegister from './guest/register'
 
 const AppWrapper = styled('div')({
   display: 'flex',
@@ -77,7 +77,6 @@ export default function App() {
           <ProtectedRoute path="/register">
             <Register />
           </ProtectedRoute>
-          <Route path="/invitelink/" component={InviteLink} />
           <Route path="/invite/logout" component={LogoutInviteSession} />
           <Route path="/invite/" component={Invite} />
           <Route path="/guestregister" component={GuestRegister} />
diff --git a/frontend/src/routes/invite/index.tsx b/frontend/src/routes/invite/index.tsx
index ba43e2d7..14acbc64 100644
--- a/frontend/src/routes/invite/index.tsx
+++ b/frontend/src/routes/invite/index.tsx
@@ -1,25 +1,27 @@
+import { useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import Page from 'components/page'
+import Loading from 'components/loading'
 
 import { styled } from '@mui/material/styles'
 
+import { useUserContext } from 'contexts'
 import { HrefButton } from 'components/button'
 import { HrefLineButton } from 'components/button/linebutton'
+import { submitJsonOpts } from 'utils'
 
 const FlexDiv = styled('div')(() => ({
   display: 'flex',
   gap: '0.5rem',
 }))
 
-function Invite() {
+function ChooseRegistrationMethod() {
   const { t } = useTranslation(['invite'])
 
   return (
     <Page>
       <h1>{t('header')}</h1>
-      <p>
-        {t('description')}
-      </p>
+      <p>{t('description')}</p>
       <FlexDiv>
         <HrefButton to="/oidc/authenticate/">{t('login')}</HrefButton>
         <HrefLineButton to="/guestregister/">{t('manual')}</HrefLineButton>
@@ -28,4 +30,125 @@ function Invite() {
   )
 }
 
+interface ShowFeedbackProps {
+  title: string
+  description: string
+}
+
+function ShowFeedback(props: ShowFeedbackProps) {
+  const { title, description } = props
+
+  return (
+    <Page>
+      <h1>{title}</h1>
+      <p>{description}</p>
+    </Page>
+  )
+}
+
+function Invite() {
+  const { t } = useTranslation(['invite'])
+  const { user, fetchUserInfo } = useUserContext()
+  const [inviteToken, setInviteToken] = useState('')
+  const [tokenChecked, setTokenChecked] = useState(false)
+  const [isCheckingToken, setIsCheckingToken] = useState(false)
+  const [tokenOk, setTokenOk] = useState(false)
+  const [checkError, setCheckError] = useState('')
+
+  async function checkToken(token: string) {
+    try {
+      const response = await fetch(
+        '/api/ui/v1/invitecheck/',
+        submitJsonOpts('POST', { invite_token: token })
+      )
+      if (response.status === 200) {
+        setTokenOk(true)
+        return
+      }
+      const data = await response.json()
+
+      if ('code' in data) {
+        setCheckError(data.code)
+      } else {
+        setCheckError('unknown')
+      }
+    } catch (error) {
+      console.error(error)
+      setCheckError('unknown')
+    } finally {
+      setTokenChecked(true)
+      setIsCheckingToken(false)
+    }
+  }
+
+  console.log({ inviteToken, isCheckingToken, tokenChecked, tokenOk, user })
+
+  useEffect(() => {
+    setIsCheckingToken(true)
+    // This may seem unecessary, but race conditions have been
+    // observed where the userinfo endpoint is called too fast
+    // and no invite_id is found in the server-side session
+    setTimeout(fetchUserInfo, 100)
+  }, [setTokenOk])
+
+  if (user.auth) {
+    return <ChooseRegistrationMethod />
+  }
+
+  if (isCheckingToken || (tokenOk && !user.auth)) {
+    return <Loading />
+  }
+
+  if (inviteToken !== '' && !tokenChecked) {
+    checkToken(inviteToken)
+    return <Loading />
+  }
+
+  if (checkError !== '') {
+    if (
+      checkError === 'missing_invite_token' ||
+      checkError === 'invalid_invite_token'
+    ) {
+      // Missing or invalid token
+      return (
+        <ShowFeedback
+          title={t('common:error.error')}
+          description={t('invite:errors.invalidToken')}
+        />
+      )
+    }
+    if (checkError === 'expired_invite_token') {
+      // Expired token
+      return (
+        <ShowFeedback
+          title={t('common:error.error')}
+          description={t('invite:errors.expiredToken')}
+        />
+      )
+    }
+    // Unknown error
+    return (
+      <ShowFeedback
+        title={t('common:error.error')}
+        description={t('common:error.unknown')}
+      />
+    )
+  }
+
+  const providedToken = window.location.hash.slice(1).trim()
+
+  if (!inviteToken && providedToken) {
+    setInviteToken(providedToken)
+    return <Loading />
+  }
+
+  // We'll end up here if no token was provided in the URL
+  return (
+    <ShowFeedback
+      title={t('common:error.error')}
+      description={t('invite:errors.invalidToken')}
+    />
+  )
+}
+
 export default Invite
diff --git a/frontend/src/routes/invitelink/logout.tsx b/frontend/src/routes/invite/logout.tsx
similarity index 100%
rename from frontend/src/routes/invitelink/logout.tsx
rename to frontend/src/routes/invite/logout.tsx
diff --git a/frontend/src/routes/invitelink/index.tsx b/frontend/src/routes/invitelink/index.tsx
deleted file mode 100644
index fd8d63f0..00000000
--- a/frontend/src/routes/invitelink/index.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useEffect } from 'react'
-import { Redirect } from 'react-router-dom'
-import { submitJsonOpts, setCookie } from 'utils'
-
-function InviteLink() {
-  // Fetch backend endpoint to preserve invite_id in backend session then redirect
-  // to generic invite page with info about feide login or manual with passport.
-
-  const id = window.location.hash.slice(1)
-
-  useEffect(() => {
-    fetch('/api/ui/v1/invitecheck/', submitJsonOpts('POST', { uuid: id }))
-  }, [])
-  setCookie('redirect', '/guestregister')
-  return <Redirect to="/invite" />
-}
-
-export default InviteLink
diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py
index a979d534..34b6d4fa 100644
--- a/gregui/api/views/invitation.py
+++ b/gregui/api/views/invitation.py
@@ -118,15 +118,33 @@ class CheckInvitationView(APIView):
 
         Uses post to prevent invite id from showing up in various logs.
         """
-        invite_id = request.data.get("uuid")
+        invite_id = request.data.get("invite_token")
         if not invite_id:
-            return Response(status=status.HTTP_403_FORBIDDEN)
+            return Response(
+                status=status.HTTP_400_BAD_REQUEST,
+                data={
+                    "code": "missing_invite_token",
+                    "message": "An invite token is required",
+                },
+            )
         try:
             invite_link = InvitationLink.objects.get(uuid=invite_id)
         except (InvitationLink.DoesNotExist, exceptions.ValidationError):
-            return Response(status=status.HTTP_403_FORBIDDEN)
+            return Response(
+                status=status.HTTP_403_FORBIDDEN,
+                data={
+                    "code": "invalid_invite_token",
+                    "message": "Invite token is invalid",
+                },
+            )
         if invite_link.expire <= timezone.now():
-            return Response(status=status.HTTP_403_FORBIDDEN)
+            return Response(
+                status=status.HTTP_403_FORBIDDEN,
+                data={
+                    "code": "expired_invite_token",
+                    "message": "Invite token has expired",
+                },
+            )
         request.session["invite_id"] = invite_id
         return Response(status=status.HTTP_200_OK)
 
diff --git a/gregui/api/views/userinfo.py b/gregui/api/views/userinfo.py
index bba967c5..f376fb0a 100644
--- a/gregui/api/views/userinfo.py
+++ b/gregui/api/views/userinfo.py
@@ -21,8 +21,8 @@ class UserInfoView(APIView):
     Quick draft, we might want to expand this later.
     """
 
-    authentication_classes: Sequence[Type[BaseAuthentication]] = [SessionAuthentication]
-    permission_classes: Sequence[Type[BasePermission]] = [AllowAny]
+    authentication_classes = [SessionAuthentication]
+    permission_classes = [AllowAny]
 
     def get(self, request, format=None):
         """
@@ -32,6 +32,7 @@ class UserInfoView(APIView):
         invitation id. Pure django users, and anonymous users are denied access.
         """
         user = request.user
+
         invite_id = request.session.get("invite_id")
 
         auth_type = "invite"
@@ -40,6 +41,8 @@ class UserInfoView(APIView):
 
         person = None
         sponsor = None
+        user_profile = None
+
         content = {
             "feide_id": None,
             "sponsor_id": None,
@@ -48,34 +51,57 @@ class UserInfoView(APIView):
             "auth_type": auth_type,
         }
 
-        # Fetch sponsor and/or person object from profile of authenticated user
         if user.is_authenticated:
             try:
-                user_profile = GregUserProfile.objects.get(user=user)
-                sponsor = user_profile.sponsor
-                person = user_profile.person
-                content.update(
-                    {
-                        "feide_id": user_profile.userid_feide,
-                    }
-                )
+                user_profile = GregUserProfile.objects.select_related(
+                    "person", "sponsor"
+                ).get(user=user)
             except GregUserProfile.DoesNotExist:
-                return Response(status=HTTP_403_FORBIDDEN)
+                return Response(
+                    status=HTTP_403_FORBIDDEN,
+                    data={
+                        "code": "no_user_profile",
+                        "message": "Authenticated, but missing user profile",
+                    },
+                )
 
+        # Fetch sponsor and/or person object from profile of authenticated user
+        if user_profile:
+            sponsor = user_profile.sponsor
+            person = user_profile.person
+            content["feide_id"] = user_profile.userid_feide
         # Or fetch person info for invited guest
         elif invite_id:
-            link = InvitationLink.objects.get(uuid=invite_id)
+            link = InvitationLink.objects.select_related(
+                "invitation__role__person",
+            ).get(uuid=invite_id)
             person = link.invitation.role.person
-
         # Otherwise, deny access
         else:
             return Response(status=HTTP_403_FORBIDDEN)
 
-        # Add sponsor fields if sponsor object present
         if sponsor:
-            content.update({"sponsor_id": user_profile.sponsor.id})
-        # Add person fields if person object present
+            content["sponsor_id"] = sponsor.id
+
         if person:
+            person_roles = [
+                {
+                    "id": role.id,
+                    "ou_nb": role.orgunit.name_nb,
+                    "ou_en": role.orgunit.name_en,
+                    "name_nb": role.type.name_nb,
+                    "name_en": role.type.name_en,
+                    "start_date": role.start_date,
+                    "end_date": role.end_date,
+                    "sponsor": {
+                        "first_name": role.sponsor.first_name,
+                        "last_name": role.sponsor.last_name,
+                    },
+                }
+                for role in person.roles.all().select_related(
+                    "orgunit", "type", "sponsor"
+                )
+            ]
             content.update(
                 {
                     "person_id": person.id,
@@ -86,22 +112,7 @@ class UserInfoView(APIView):
                     and person.private_mobile.value,
                     "fnr": person.fnr and "".join((person.fnr.value[:-5], "*****")),
                     "passport": person.passport and person.passport.value,
-                    "roles": [
-                        {
-                            "id": role.id,
-                            "ou_nb": role.orgunit.name_nb,
-                            "ou_en": role.orgunit.name_en,
-                            "name_nb": role.type.name_nb,
-                            "name_en": role.type.name_en,
-                            "start_date": role.start_date,
-                            "end_date": role.end_date,
-                            "sponsor": {
-                                "first_name": role.sponsor.first_name,
-                                "last_name": role.sponsor.last_name,
-                            },
-                        }
-                        for role in person.roles.all()
-                    ],
+                    "roles": person_roles,
                 }
             )
         return Response(content)
diff --git a/gregui/tests/api/views/test_invitation.py b/gregui/tests/api/views/test_invitation.py
index 8a10b372..0fb8e456 100644
--- a/gregui/tests/api/views/test_invitation.py
+++ b/gregui/tests/api/views/test_invitation.py
@@ -12,15 +12,21 @@ from greg.models import Consent, InvitationLink, Person, Identity
 @pytest.mark.django_db
 def test_post_invite(client):
     """Forbid access with bad invitation link uuid"""
-    response = client.post(reverse("gregui-v1:invite-verify"), data={"uuid": "baduuid"})
+    response = client.post(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": "baduuid"}
+    )
     assert response.status_code == status.HTTP_403_FORBIDDEN
+    assert response.json() == {
+        "code": "invalid_invite_token",
+        "message": "Invite token is invalid",
+    }
 
 
 @pytest.mark.django_db
 def test_post_invite_ok(client, invitation_link):
     """Access okay with valid invitation link"""
     response = client.post(
-        reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid}
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
     )
     assert response.status_code == status.HTTP_200_OK
 
@@ -29,23 +35,34 @@ def test_post_invite_ok(client, invitation_link):
 def test_post_invite_expired(client, invitation_link_expired):
     """Forbid access with expired invite link"""
     response = client.post(
-        reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link_expired.uuid}
+        reverse("gregui-v1:invite-verify"),
+        data={"invite_token": invitation_link_expired.uuid},
     )
     assert response.status_code == status.HTTP_403_FORBIDDEN
+    assert response.json() == {
+        "code": "expired_invite_token",
+        "message": "Invite token has expired",
+    }
 
 
 @pytest.mark.django_db
 def test_post_missing_invite_id(client):
     """Forbid access if no id provided."""
     response = client.post(reverse("gregui-v1:invite-verify"))
-    assert response.status_code == status.HTTP_403_FORBIDDEN
+    assert response.status_code == status.HTTP_400_BAD_REQUEST
+    assert response.json() == {
+        "code": "missing_invite_token",
+        "message": "An invite token is required",
+    }
 
 
 @pytest.mark.django_db
 def test_get_invited_info_no_session(client, invitation_link):
     """Forbid access if invite expired after session was created."""
     # get a session
-    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
+    client.post(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
+    )
     # expire the invitation link
     invitation_link.expire = timezone.now() - datetime.timedelta(days=1)
     invitation_link.save()
@@ -60,7 +77,9 @@ def test_get_invited_info_session_okay(
 ):
     person, invitation_link = invited_person
     # get a session
-    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
+    client.post(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
+    )
     # Get info
     response = client.get(reverse("gregui-v1:invited-info"))
     assert response.status_code == status.HTTP_200_OK
@@ -95,7 +114,9 @@ def test_get_invited_info_expired_link(
     client, invitation_link, invitation_expired_date
 ):
     # Get a session while link is valid
-    client.get(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
+    client.get(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
+    )
     # Set expire link to expire long ago
     invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
     invlink.expire = invitation_expired_date
@@ -110,7 +131,9 @@ def test_get_invited_info_expired_link(
 def test_invited_guest_can_post_information(client: APIClient, invited_person):
     person, invitation_link = invited_person
     # get a session
-    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
+    client.post(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
+    )
 
     person = invitation_link.invitation.role.person
     assert person.private_mobile is None
@@ -138,7 +161,9 @@ def test_post_invited_info_expired_session(
     client, invitation_link, invitation_expired_date
 ):
     # get a session
-    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
+    client.post(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
+    )
     # Set expire link to expire long ago
     invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
     invlink.expire = invitation_expired_date
@@ -152,7 +177,9 @@ def test_post_invited_info_expired_session(
 @pytest.mark.django_db
 def test_post_invited_info_deleted_inv_link(client, invitation_link):
     # get a session
-    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
+    client.post(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
+    )
     # Delete link
     invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
     invlink.delete()
@@ -284,7 +311,9 @@ def test_name_update_not_allowed_if_feide_identity_is_present(
     )
     invitation = create_invitation(role)
     invitation_link = create_invitation_link(invitation, invitation_valid_date)
-    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
+    client.post(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
+    )
 
     person = invitation_link.invitation.role.person
     data = {
@@ -330,7 +359,9 @@ def test_name_update_allowed_if_feide_identity_is_not_present(
     invitation_link = create_invitation_link(
         create_invitation(role), invitation_valid_date
     )
-    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
+    client.post(
+        reverse("gregui-v1:invite-verify"), data={"invite_token": invitation_link.uuid}
+    )
 
     person = invitation_link.invitation.role.person
     data = {
diff --git a/gregui/tests/api/views/test_userinfo.py b/gregui/tests/api/views/test_userinfo.py
index cf1f331d..df7d3cb4 100644
--- a/gregui/tests/api/views/test_userinfo.py
+++ b/gregui/tests/api/views/test_userinfo.py
@@ -6,7 +6,7 @@ from rest_framework import status
 @pytest.mark.django_db
 def test_userinfo_anon_get(client):
     """Anonymous people should be forbidden."""
-    response = client.get(reverse("api-userinfo"))
+    response = client.get(reverse("gregui-v1:userinfo"))
     assert response.status_code == status.HTTP_403_FORBIDDEN
 
 
@@ -16,7 +16,7 @@ def test_userinfo_invited_get(client, invitation_link):
     session = client.session
     session["invite_id"] = str(invitation_link.uuid)
     session.save()
-    response = client.get(reverse("api-userinfo"))
+    response = client.get(reverse("gregui-v1:userinfo"))
     assert response.status_code == status.HTTP_200_OK
     assert response.json() == {
         "auth_type": "invite",
@@ -48,7 +48,7 @@ def test_userinfo_invited_get(client, invitation_link):
 def test_userinfo_user_no_profile(client, log_in, user_no_profile):
     """Pure django users should be forbidden"""
     log_in(user_no_profile)
-    response = client.get(reverse("api-userinfo"))
+    response = client.get(reverse("gregui-v1:userinfo"))
     assert response.status_code == status.HTTP_403_FORBIDDEN
 
 
@@ -57,7 +57,7 @@ def test_userinfo_sponsor_get(client, log_in, user_sponsor):
     """Sponsors should get info about themselves"""
     log_in(user_sponsor)
 
-    response = client.get(reverse("api-userinfo"))
+    response = client.get(reverse("gregui-v1:userinfo"))
     assert response.status_code == status.HTTP_200_OK
     assert response.json() == {
         "auth_type": "oidc",
@@ -72,7 +72,7 @@ def test_userinfo_sponsor_get(client, log_in, user_sponsor):
 def test_userinfo_guest_get(client, log_in, user_person):
     """Logged in guests should get info about themself"""
     log_in(user_person)
-    response = client.get(reverse("api-userinfo"))
+    response = client.get(reverse("gregui-v1:userinfo"))
     assert response.status_code == status.HTTP_200_OK
     assert response.json() == {
         "auth_type": "oidc",
-- 
GitLab