diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 611406464b289565a84927754557afb94dcabe13..6187593e70c7ad471ab02555a480d1d7d5b79ca6 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -156,6 +156,15 @@ "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.", - "unknown": "An unknown error has occurred. If the problem persists, contact support." + "unknown": "An unknown error has occurred. If the problem persists, contact support.", + "contactHelp": "Contact help through the link in the footer if the problem persists.", + "invitationDataFetchFailed": "Failed to fetch invitation data", + "guestRegistrationFailed": "Failed to register your data", + "codes": { + "invalid_invite": "Invalid invite", + "invite_expired": "Invite has expired", + "cannot_update_fields": "Failed to update data", + "update_national_id_not_allowed": "Not allowed to update verified national ID" + } } } diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index b9f2d0a7e70dd9bc4e59fd1f66e55eb4857b644a..4d1855ffe9800332aed8db0bafa13fd7081e7472 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -156,6 +156,15 @@ "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)", "genericServerErrorBody": "Respons fra server:<1>{{errorBodyText}}</1>", "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", - "unknown": "En ukjent feil har oppstått. Om problemet vedvarer, kontakt brukerstøtte." + "unknown": "En ukjent feil har oppstått. Om problemet vedvarer, kontakt brukerstøtte.", + "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", + "invitationDataFetchFailed": "Klarte ikke å hente invitasjonsdata", + "guestRegistrationFailed": "Klarte ikke å registrere dataene dine", + "codes": { + "invalid_invite": "Ugyldig invitasjon", + "invite_expired": "Invitasjonen har utløpt", + "cannot_update_fields": "Klarte ikke å oppdatere dataene", + "update_national_id_not_allowed": "Ikke tillatt å endre verifisert fødselsnummer/D-nummer" + } } } diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index 60fc0cc01df242b2171d6885213c7bfe3bc8c53c..23ad489b0b76af9166004dda686044e4d52fd062 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -157,6 +157,15 @@ "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)", "genericServerErrorBody": "Respons frå server:<1>{{errorBodyText}}</1>", "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", - "unknown": "Ein uventa feil oppstod. Om problemet varer ved, kontakt brukarstøtte." + "unknown": "Ein uventa feil oppstod. Om problemet varer ved, kontakt brukarstøtte.",, + "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", + "invitationDataFetchFailed": "Klarte ikkje å hente invitasjonsdata", + "guestRegistrationFailed": "Klarte ikkje å registrere dataene dine", + "codes": { + "invalid_invite": "Ugyldig invitasjon", + "invite_expired": "Invitasjonen har utløpe", + "cannot_update_fields": "Klarte ikkje å oppdatere dataene", + "update_national_id_not_allowed": "Ikkje tillete å endre verifisert fødselsnummer/D-nummer" + } } } diff --git a/frontend/src/components/errorReport/index.tsx b/frontend/src/components/errorReport/index.tsx index 4aace3fff5168fe2edd63667db6240d5fc2420dc..32d0f67fe1f4aadb1f5c03b69cd87000a2361753 100644 --- a/frontend/src/components/errorReport/index.tsx +++ b/frontend/src/components/errorReport/index.tsx @@ -2,7 +2,7 @@ import { Alert, AlertTitle } from '@mui/material' import { Trans, useTranslation } from 'react-i18next' import React from 'react' -interface ErrorReportProps { +export interface ErrorReportProps { errorHeading: string statusCode?: number statusText?: string diff --git a/frontend/src/routes/guest/register/index.tsx b/frontend/src/routes/guest/register/index.tsx index c595bac7361717600f2670694d96eec2852f3782..04348238914c95d73025a7d78454939681e08d42 100644 --- a/frontend/src/routes/guest/register/index.tsx +++ b/frontend/src/routes/guest/register/index.tsx @@ -21,12 +21,9 @@ import AuthenticationMethod from './authenticationMethod' import GuestRegisterStep from './steps/register' import GuestConsentStep from './steps/consent' import GuestSuccessStep from './steps/success' - -enum SubmitState { - NotSubmitted, - Submitted, - SubmittedError, -} +import ServerErrorReport, { + ErrorReportProps, +} from '../../../components/errorReport' enum Step { RegisterStep, @@ -72,12 +69,6 @@ export default function GuestRegister() { const guestRegisterRef = useRef<GuestRegisterCallableMethods>(null) const guestConsentRef = useRef<GuestRegisterCallableMethods>(null) - - // TODO On submit successful the user should be directed to some page telling - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [submitState, setSubmitState] = useState<SubmitState>( - SubmitState.NotSubmitted - ) const [activeStep, setActiveStep] = useState(0) const [initialGuestData, setInitialGuestData] = @@ -87,12 +78,39 @@ export default function GuestRegister() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [guestConsentData, setGuestConsentData] = useState<GuestConsentData | null>(null) + const [fetchInvitationDataError, setFetchInvitationDataError] = + useState<ErrorReportProps | null>(null) + const [submitGuestDataError, setSubmitGuestDataError] = + useState<ErrorReportProps | null>(null) const fetchInvitationData = async () => { const response = await fetch('/api/ui/v1/invited/', fetchJsonOpts()) if (!response.ok) { + try { + // Expect that the error will contain some JSON-data in the body + const errorData = await response.json() + setFetchInvitationDataError({ + errorHeading: t('error.invitationDataFetchFailed'), + statusCode: response.status, + statusText: response.statusText, + errorBodyText: t(`error.codes.${errorData?.code}`), + }) + } catch (e: unknown) { + console.error(e) + + // Probably some unknown data in the body, create an error message + // using the rest of the information in the response + setFetchInvitationDataError({ + errorHeading: t('error.invitationDataFetchFailed'), + statusCode: response.status, + statusText: response.statusText, + errorBodyText: undefined, + }) + } + return } + setFetchInvitationDataError(null) const data: InvitationData = await response.json() const authenticationMethod = @@ -192,6 +210,8 @@ export default function GuestRegister() { const handleBack = () => { if (activeStep === Step.ConsentStep) { setActiveStep(Step.RegisterStep) + // Clear error if any + setSubmitGuestDataError(null) } } @@ -257,15 +277,43 @@ export default function GuestRegister() { fetch('/api/ui/v1/invited/', submitJsonOpts('POST', payload)) .then((response) => { if (response.ok) { - setSubmitState(SubmitState.Submitted) setActiveStep(Step.SuccessStep) + setSubmitGuestDataError(null) } else { - setSubmitState(SubmitState.SubmittedError) - console.error(`Server responded with status: ${response.status}`) + // Expect that the error will contain JSON-data in the body + // and that there will be a code field there + response + .json() + .then((errorData) => { + setSubmitGuestDataError({ + errorHeading: t('error.guestRegistrationFailed'), + statusCode: response.status, + statusText: response.statusText, + errorBodyText: t(`error.codes.${errorData?.code}`), + }) + }) + .catch((error) => { + console.error(error) + + // Probably some unknown data in the body, create an error message + // using the rest of the information in the response + setSubmitGuestDataError({ + errorHeading: t('error.guestRegistrationFailed'), + statusCode: response.status, + statusText: response.statusText, + errorBodyText: undefined, + }) + }) } }) .catch((error) => { - setSubmitState(SubmitState.SubmittedError) + // Something went wrong before/during the backend was called + setSubmitGuestDataError({ + errorHeading: t('error.guestRegistrationFailed'), + statusCode: undefined, + statusText: undefined, + errorBodyText: undefined, + }) console.error(error) }) } @@ -372,6 +420,24 @@ export default function GuestRegister() { </Button> )} </Box> + + {fetchInvitationDataError !== null && ( + <ServerErrorReport + errorHeading={fetchInvitationDataError?.errorHeading} + statusCode={fetchInvitationDataError?.statusCode} + statusText={fetchInvitationDataError?.statusText} + errorBodyText={fetchInvitationDataError?.errorBodyText} + /> + )} + + {submitGuestDataError !== null && ( + <ServerErrorReport + errorHeading={submitGuestDataError?.errorHeading} + statusCode={submitGuestDataError?.statusCode} + statusText={submitGuestDataError?.statusText} + errorBodyText={submitGuestDataError?.errorBodyText} + /> + )} </Page> ) } diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py index 34b6d4fa79bc078e488b1f6e5b70f176761b6da6..61c77af66dd9eb22016b00cef539eaa94988631e 100644 --- a/gregui/api/views/invitation.py +++ b/gregui/api/views/invitation.py @@ -196,9 +196,15 @@ class InvitedGuestView(GenericAPIView): 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", "message": "Invalid invite"}, + ) if invite_link.expire <= timezone.now(): - return Response(status=status.HTTP_403_FORBIDDEN) + return Response( + status=status.HTTP_403_FORBIDDEN, + data={"code": "invite_expired", "message": "Invite expired"}, + ) # if invite_id: invite_link = InvitationLink.objects.get(uuid=invite_id) @@ -275,12 +281,21 @@ class InvitedGuestView(GenericAPIView): if illegal_fields: return Response( status=status.HTTP_400_BAD_REQUEST, - data={"error": {"cannot_update_fields": illegal_fields}}, + data={ + "code": "cannot_update_fields", + "message": f"cannot_update_fields: {illegal_fields}", + }, ) if self._verified_fnr_already_exists(person) and fnr: # The user should not be allowed to change a verified fnr - return Response(status=status.HTTP_400_BAD_REQUEST) + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "code": "update_national_id_not_allowed", + "message": "Not allowed to update verified national ID", + }, + ) with transaction.atomic(): # Note this only serializes data for the person, it does not look at other sections