From a3a7410d6f07628f4ce8a6b216f2821f795b55c2 Mon Sep 17 00:00:00 2001
From: Andreas Ellewsen <ae@uio.no>
Date: Mon, 22 Nov 2021 15:02:42 +0100
Subject: [PATCH] Add identity verification to frontend

The profile page of a guest now shows a verification button if the guest
has a passport or national identificaiton number that has not been
verified. Clicking the button shows a dialog, with a confirmation button
which triggers a PATCH request to the backend and reloads the page when
it returns.

Resolves: GREG-101
---
 .../src/components/confirmDialog/index.tsx    | 50 +++++++++++++++++
 .../src/components/identityLine/index.tsx     | 56 +++++++++++++++++++
 frontend/src/hooks/useGuest/index.tsx         |  8 ++-
 frontend/src/hooks/useGuests/index.tsx        |  7 ++-
 frontend/src/interfaces/index.ts              | 20 ++++++-
 .../routes/sponsor/guest/guestInfo/index.tsx  | 18 ++++--
 frontend/src/utils/index.ts                   | 16 +++++-
 gregui/api/views/person.py                    | 15 ++++-
 gregui/views.py                               | 15 ++++-
 9 files changed, 189 insertions(+), 16 deletions(-)
 create mode 100644 frontend/src/components/confirmDialog/index.tsx
 create mode 100644 frontend/src/components/identityLine/index.tsx

diff --git a/frontend/src/components/confirmDialog/index.tsx b/frontend/src/components/confirmDialog/index.tsx
new file mode 100644
index 00000000..9bbff7da
--- /dev/null
+++ b/frontend/src/components/confirmDialog/index.tsx
@@ -0,0 +1,50 @@
+import {
+  Button,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+} from '@mui/material'
+import React from 'react'
+
+type ConfirmDialogProps = {
+  title: string
+  open: boolean
+  setOpen: (value: boolean) => void
+  onConfirm: () => void
+  children: React.ReactNode
+}
+
+const ConfirmDialog = (props: ConfirmDialogProps) => {
+  const { title, children, open, setOpen, onConfirm } = props
+  return (
+    <Dialog
+      open={open}
+      onClose={() => setOpen(false)}
+      aria-labelledby="confirm-dialog"
+    >
+      <DialogTitle id="confirm-dialog">{title}</DialogTitle>
+      <DialogContent>{children}</DialogContent>
+      <DialogActions>
+        <Button
+          variant="contained"
+          onClick={() => setOpen(false)}
+          color="secondary"
+        >
+          No
+        </Button>
+        <Button
+          variant="contained"
+          onClick={() => {
+            setOpen(false)
+            onConfirm()
+          }}
+          color="error"
+        >
+          Yes
+        </Button>
+      </DialogActions>
+    </Dialog>
+  )
+}
+export default ConfirmDialog
diff --git a/frontend/src/components/identityLine/index.tsx b/frontend/src/components/identityLine/index.tsx
new file mode 100644
index 00000000..c36ce0cc
--- /dev/null
+++ b/frontend/src/components/identityLine/index.tsx
@@ -0,0 +1,56 @@
+import { Button, TableCell, TableRow } from '@mui/material'
+import ConfirmDialog from 'components/confirmDialog'
+import { Identity } from 'interfaces'
+import { useState } from 'react'
+import { submitJsonOpts } from 'utils'
+import CheckIcon from '@mui/icons-material/Check'
+import { useHistory } from 'react-router-dom'
+
+interface IdentityLineProps {
+  text: string
+  identity: Identity | null
+}
+const IdentityLine = ({ text, identity }: IdentityLineProps) => {
+  // Make a line with a confirmation button if the identity has not been verified
+  const [confirmOpen, setConfirmOpen] = useState(false)
+  const history = useHistory()
+  const verifyIdentity = (id: string) => async () => {
+    await fetch(`/api/ui/v1/identity/${id}`, submitJsonOpts('PATCH', {}))
+    history.go(0)
+  }
+
+  if (identity == null) {
+    return <></>
+  }
+  return (
+    <TableRow>
+      <TableCell align="left">{text}</TableCell>
+      <TableCell align="left">{identity ? identity.value : ''}</TableCell>
+      <TableCell>
+        {!identity.verified_at ? (
+          <div>
+            <Button
+              aria-label="verify"
+              onClick={() => setConfirmOpen(true)}
+              disabled={!identity}
+            >
+              Verify
+            </Button>
+            <ConfirmDialog
+              title="TODO transaltion Confirm?"
+              open={confirmOpen}
+              setOpen={setConfirmOpen}
+              onConfirm={verifyIdentity(identity.id)}
+            >
+              TODO: translation Are you sure you want to verify this identity?
+            </ConfirmDialog>
+          </div>
+        ) : (
+          <CheckIcon sx={{ fill: 'green' }} />
+        )}
+      </TableCell>
+    </TableRow>
+  )
+}
+
+export default IdentityLine
diff --git a/frontend/src/hooks/useGuest/index.tsx b/frontend/src/hooks/useGuest/index.tsx
index b719ca0a..62cf6e5b 100644
--- a/frontend/src/hooks/useGuest/index.tsx
+++ b/frontend/src/hooks/useGuest/index.tsx
@@ -1,6 +1,6 @@
 import { FetchedRole, Guest } from 'interfaces'
 import { useEffect, useState } from 'react'
-import { parseRole } from 'utils'
+import { parseRole, parseIdentity } from 'utils'
 
 const useGuest = (pid: string) => {
   const [guestInfo, setGuest] = useState<Guest>({
@@ -8,7 +8,8 @@ const useGuest = (pid: string) => {
     first: '',
     last: '',
     email: '',
-    fnr: '',
+    fnr: null,
+    passport: null,
     mobile: '',
     active: false,
     registered: false,
@@ -28,7 +29,8 @@ const useGuest = (pid: string) => {
           last: rjson.last,
           email: rjson.email,
           mobile: rjson.mobile,
-          fnr: rjson.fnr,
+          fnr: parseIdentity(rjson.fnr),
+          passport: parseIdentity(rjson.passport),
           active: rjson.active,
           registered: rjson.registered,
           verified: rjson.verified,
diff --git a/frontend/src/hooks/useGuests/index.tsx b/frontend/src/hooks/useGuests/index.tsx
index d18b2465..58a7b1c3 100644
--- a/frontend/src/hooks/useGuests/index.tsx
+++ b/frontend/src/hooks/useGuests/index.tsx
@@ -1,6 +1,6 @@
 import { useEffect, useState } from 'react'
-import { FetchedGuest, Guest } from '../../interfaces'
-import { parseRole } from '../../utils'
+import { FetchedGuest, Guest } from 'interfaces'
+import { parseIdentity, parseRole } from 'utils'
 
 const useGuests = () => {
   const [guests, setGuests] = useState<Guest[]>([])
@@ -19,7 +19,8 @@ const useGuests = () => {
               last: person.last,
               email: person.email,
               mobile: person.mobile,
-              fnr: person.fnr,
+              fnr: parseIdentity(person.fnr),
+              passport: parseIdentity(person.passport),
               active: person.active,
               roles: person.roles.map((role) => parseRole(role)),
               registered: person.registered,
diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts
index 242b6785..9cd159ff 100644
--- a/frontend/src/interfaces/index.ts
+++ b/frontend/src/interfaces/index.ts
@@ -4,20 +4,36 @@ export type Guest = {
   last: string
   email: string
   mobile: string
-  fnr: string
+  fnr: Identity | null
+  passport: Identity | null
   active: boolean
   registered: boolean
   verified: boolean
   roles: Role[]
 }
 
+export type Identity = {
+  id: string
+  type: string
+  verified_at: Date | null
+  value: string
+}
+
+export type FetchedIdentity = {
+  id: string
+  type: string
+  verified_at: string | null
+  value: string
+}
+
 export interface FetchedGuest {
   pid: string
   first: string
   last: string
   email: string
   mobile: string
-  fnr: string
+  fnr: FetchedIdentity | null
+  passport: FetchedIdentity | null
   active: boolean
   registered: boolean
   verified: boolean
diff --git a/frontend/src/routes/sponsor/guest/guestInfo/index.tsx b/frontend/src/routes/sponsor/guest/guestInfo/index.tsx
index d01f6f02..73b83d10 100644
--- a/frontend/src/routes/sponsor/guest/guestInfo/index.tsx
+++ b/frontend/src/routes/sponsor/guest/guestInfo/index.tsx
@@ -23,7 +23,8 @@ import { Guest } from 'interfaces'
 import SponsorInfoButtons from 'routes/components/sponsorInfoButtons'
 import { useEffect, useState } from 'react'
 import { SubmitHandler, useForm } from 'react-hook-form'
-import { isValidEmail, submitJsonOpts } from '../../../../utils'
+import IdentityLine from 'components/identityLine'
+import { isValidEmail, submitJsonOpts } from 'utils'
 
 type GuestInfoParams = {
   pid: string
@@ -164,6 +165,7 @@ export default function GuestInfo({
             <TableRow>
               <TableCell align="left">{t('guestInfo.contactInfo')}</TableCell>
               <TableCell />
+              <TableCell />
             </TableRow>
           </TableHead>
           <TableBody>
@@ -172,6 +174,7 @@ export default function GuestInfo({
               <TableCell align="left">
                 {`${guest.first} ${guest.last}`}
               </TableCell>
+              <TableCell />
             </TableRow>
             <TableRow>
               <TableCell align="left">{t('input.email')}</TableCell>
@@ -224,11 +227,16 @@ export default function GuestInfo({
                   onClose={handleDialogClose}
                 />
               </TableCell>
+              <TableCell />
             </TableRow>
-            <TableRow>
-              <TableCell align="left">{t('input.nationalIdNumber')}</TableCell>
-              <TableCell align="left">{guest.fnr}</TableCell>
-            </TableRow>
+            <IdentityLine
+              text={t('input.nationalIdNumber')}
+              identity={guest.fnr}
+            />
+            <IdentityLine
+              text={t('input.passportNumber')}
+              identity={guest.passport}
+            />
             <TableRow>
               <TableCell align="left">{t('input.mobilePhone')}</TableCell>
               <TableCell align="left">{guest.mobile}</TableCell>
diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts
index 08dfed2f..88b23193 100644
--- a/frontend/src/utils/index.ts
+++ b/frontend/src/utils/index.ts
@@ -1,7 +1,7 @@
 import validator from '@navikt/fnrvalidator'
 import { parseISO } from 'date-fns'
 import i18n from 'i18next'
-import { FetchedRole, Role } from 'interfaces'
+import { FetchedIdentity, FetchedRole, Identity, Role } from 'interfaces'
 import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js'
 
 const validEmailRegex =
@@ -122,3 +122,17 @@ export function parseRole(role: FetchedRole): Role {
     max_days: role.max_days,
   }
 }
+
+export function parseIdentity(
+  identity: FetchedIdentity | null
+): Identity | null {
+  if (identity == null) {
+    return null
+  }
+  return {
+    id: identity.id,
+    type: identity.type,
+    value: identity.value,
+    verified_at: identity.verified_at ? parseISO(identity.verified_at) : null,
+  }
+}
diff --git a/gregui/api/views/person.py b/gregui/api/views/person.py
index 50ed2921..efcd19b9 100644
--- a/gregui/api/views/person.py
+++ b/gregui/api/views/person.py
@@ -33,7 +33,20 @@ class PersonView(APIView):
             "last": person.last_name,
             "email": person.private_email and person.private_email.value,
             "mobile": person.private_mobile and person.private_mobile.value,
-            "fnr": person.fnr and "".join((person.fnr.value[:-5], "*****")),
+            "fnr": person.fnr
+            and {
+                "id": person.fnr.id,
+                "value": person.fnr.value,
+                "type": person.fnr.type,
+                "verified_at": person.fnr.verified_at,
+            },
+            "passport": person.passport
+            and {
+                "id": person.passport.id,
+                "value": person.passport.value,
+                "type": person.passport.type,
+                "verified_at": person.passport.verified_at,
+            },
             "active": person.is_registered and person.is_verified,
             "registered": person.is_registered,
             "verified": person.is_verified,
diff --git a/gregui/views.py b/gregui/views.py
index 4721cccb..11ce1973 100644
--- a/gregui/views.py
+++ b/gregui/views.py
@@ -104,7 +104,20 @@ class GuestInfoView(APIView):
                         "last": person.last_name,
                         "email": person.private_email and person.private_email.value,
                         "mobile": person.private_mobile and person.private_mobile.value,
-                        "fnr": person.fnr and "".join((person.fnr.value[:-5], "*****")),
+                        "fnr": person.fnr
+                        and {
+                            "id": person.fnr.id,
+                            "value": "".join((person.fnr.value[:-5], "*****")),
+                            "type": person.fnr.type,
+                            "verified_at": person.fnr.verified_at,
+                        },
+                        "passport": person.passport
+                        and {
+                            "id": person.passport.id,
+                            "value": person.passport.value,
+                            "type": person.passport.type,
+                            "verified_at": person.passport.verified_at,
+                        },
                         "active": person.is_registered and person.is_verified,
                         "registered": person.is_registered,
                         "verified": person.is_verified,
-- 
GitLab