From 086158414c411bdee92786b6dc034a7940591fe5 Mon Sep 17 00:00:00 2001 From: Jeffrey Mesa Date: Tue, 10 Oct 2023 16:01:39 -0400 Subject: [PATCH] refactor: use `fetch` instead of `axios` (#99) * refactor(api): use `fetch` instead of `axios` * refactor: add environment variables type definitions * refactor(api): extract message strings As preparation for internationalization. * refactor(api): use `params` instead of `queryParams` Simplify route handling by directly integrating parameters into the route structure. * refactor: improve headers handling * refactor(frontend): use `fetch` instead of `axios` * refactor: remove axios package * refactor: add types and handlers --------- Co-authored-by: Marluan Espiritusanto --- env.d.ts | 19 ++++ package.json | 1 - .../biometric/[sessionId]/[cedula]/route.ts | 95 ++++++++++++++++ src/app/api/biometric/route.ts | 105 +----------------- src/app/api/citizens/[cedula]/route.ts | 83 +++++++------- src/app/api/iam/[cedula]/route.ts | 32 ++---- src/app/api/pwned/[password]/route.ts | 53 +++++---- src/app/api/recaptcha/route.ts | 104 +++++++---------- src/app/api/types/index.ts | 2 +- src/app/api/types/recaptcha.type.ts | 8 ++ src/app/register/stepper/index.tsx | 3 +- src/app/register/stepper/step1.tsx | 36 +++--- src/app/register/stepper/step3.tsx | 12 +- .../interfaces/step-2-props.interface.ts | 4 +- .../biometric/face-liveness-detector.tsx | 24 ++-- src/helpers/index.ts | 1 + src/helpers/unwrap.ts | 1 + tsconfig.json | 3 +- yarn.lock | 25 +---- 19 files changed, 297 insertions(+), 314 deletions(-) create mode 100644 env.d.ts create mode 100644 src/app/api/biometric/[sessionId]/[cedula]/route.ts create mode 100644 src/helpers/unwrap.ts diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 00000000..e017ce77 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,19 @@ +namespace NodeJS { + interface ProcessEnv { + CEDULA_API?: string; + CEDULA_API_KEY?: string; + JCE_PHOTO_API?: string; + JCE_PHOTO_API_KEY?: string; + ENCRYPTION_KEY?: string; + RECAPTHA_API_KEY?: string; + RECAPTHA_PROJECT_ID?: string; + SITE_COOKIE_KEY?: string; + NEXT_PUBLIC_RECAPTCHA_SITE_KEY?: string; + NEXT_PUBLIC_GTM_ID?: string; + CEDULA_TOKEN_API?: string; + CITIZENS_API_AUTH_KEY?: string; + NEXT_PUBLIC_ORY_SDK_URL?: string; + ORY_SDK_TOKEN?: string; + PWNED_KEY?: string; + } +} diff --git a/package.json b/package.json index 4dcbc323..3e956c99 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@types/bunyan": "^1.8.8", "@types/react-gtm-module": "^2.0.1", "aws-amplify": "^5.3.5", - "axios": "^1.4.0", "bunyan": "^1.8.15", "check-password-strength": "^2.0.7", "cryptr": "^6.2.0", diff --git a/src/app/api/biometric/[sessionId]/[cedula]/route.ts b/src/app/api/biometric/[sessionId]/[cedula]/route.ts new file mode 100644 index 00000000..2aecbbea --- /dev/null +++ b/src/app/api/biometric/[sessionId]/[cedula]/route.ts @@ -0,0 +1,95 @@ +import type { CompareFacesCommandInput } from '@aws-sdk/client-rekognition'; +import { NextRequest, NextResponse } from 'next/server'; + +import { getRekognitionClient } from '@/helpers'; +import logger from '@/lib/logger'; +import { + LIVENESS_LOW_CONFIDENCE_ERROR, + LIVENESS_NO_MATCH_ERROR, +} from '@/constants'; + +type Props = { params: { sessionId: string; cedula: string } }; + +export async function GET( + req: NextRequest, + { params: { sessionId, cedula } }: Props, +) { + const client = await getRekognitionClient(req); + const response = await client.getFaceLivenessSessionResults({ + SessionId: sessionId, + }); + + const confidence = response.Confidence ?? 0; + // Threshold for face liveness + const isLive = confidence > 85; + + if (!isLive) { + logger.warn(`Low confidence (${confidence}%) for citizen ${cedula}`); + + return NextResponse.json( + { + message: LIVENESS_LOW_CONFIDENCE_ERROR, + isLive, + }, + { status: 403 }, + ); + } + + logger.info(`High confidence (${confidence}%) for citizen ${cedula}`); + + if (response?.ReferenceImage?.Bytes) { + const targetImageBuffer = await fetchPhotoBuffer(cedula); + + try { + const params: CompareFacesCommandInput = { + SourceImage: { + Bytes: Buffer.from(response.ReferenceImage.Bytes), + }, + TargetImage: { + Bytes: Buffer.from(targetImageBuffer), + }, + // Threshold for face match + SimilarityThreshold: 95, + }; + + const { FaceMatches } = await client.compareFaces(params); + + if (!FaceMatches?.length) { + logger.warn(`Low similarity for citizen ${cedula}`); + + return NextResponse.json( + { + message: LIVENESS_NO_MATCH_ERROR, + isMatch: false, + }, + { + status: 404, + }, + ); + } + + const similarity = FaceMatches[0].Similarity; + + logger.info(`High similarity (${similarity}%) for citizen ${cedula}`); + + return NextResponse.json({ isMatch: true }); + } catch (error) { + logger.error(error); + + return NextResponse.json( + { + message: LIVENESS_NO_MATCH_ERROR, + isMatch: false, + }, + { status: 500 }, + ); + } + } +} + +const fetchPhotoBuffer = async (cedula: string) => { + const photoUrl = new URL(`${process.env.JCE_PHOTO_API!}/${cedula}/photo`); + photoUrl.searchParams.append('api-key', process.env.JCE_PHOTO_API_KEY!); + + return fetch(photoUrl).then((res) => res.arrayBuffer()); +}; diff --git a/src/app/api/biometric/route.ts b/src/app/api/biometric/route.ts index 437807c4..d0ee9e6e 100644 --- a/src/app/api/biometric/route.ts +++ b/src/app/api/biometric/route.ts @@ -1,111 +1,14 @@ import { NextRequest, NextResponse } from 'next/server'; -import axios from 'axios'; import { getRekognitionClient } from '@/helpers'; -import logger from '@/lib/logger'; -import { - LIVENESS_LOW_CONFIDENCE_ERROR, - LIVENESS_NO_MATCH_ERROR, -} from '@/constants'; - -export async function GET( - req: NextRequest, - res: NextResponse, -): Promise { - const http = axios.create({ - baseURL: process.env.JCE_PHOTO_API, - }); - const url = new URL(req.url); - - const sessionId = url.searchParams.get('sessionId'); - const cedula = url.searchParams.get('cedula'); - - const SessionId = sessionId as string; - - const client = await getRekognitionClient(req); - const response = await client.getFaceLivenessSessionResults({ - SessionId, - }); - - let isLive = false; - const confidence = response.Confidence; - - // Threshold for face liveness - if (confidence && confidence > 85) { - logger.info(`High confidence (${confidence}%) for citizen ${cedula}`); - isLive = true; - } else { - logger.warn(`Low confidence (${confidence}%) for citizen ${cedula}`); - return NextResponse.json({ - message: LIVENESS_LOW_CONFIDENCE_ERROR, - isLive: isLive, - status: 200, - }); - } - - if (isLive && response.ReferenceImage && response.ReferenceImage.Bytes) { - const { data } = await http.get(`/${cedula}/photo`, { - params: { - 'api-key': process.env.JCE_PHOTO_API_KEY, - }, - responseType: 'arraybuffer', - }); - - const buffer1 = Buffer.from(response.ReferenceImage.Bytes); - const buffer2 = Buffer.from(data, 'base64'); - const params = { - SourceImage: { - Bytes: buffer1, - }, - TargetImage: { - Bytes: buffer2, - }, - // Threshold for face match - SimilarityThreshold: 95, - }; - - try { - const response = await client.compareFaces(params); - if (response.FaceMatches && response.FaceMatches.length) { - const similarity = response.FaceMatches[0].Similarity; - logger.info(`High similarity (${similarity}%) for citizen ${cedula}`); - return NextResponse.json({ - isMatch: true, - status: 200, - }); - } else { - logger.warn(`Low similarity for citizen ${cedula}`); - return NextResponse.json({ - message: LIVENESS_NO_MATCH_ERROR, - isMatch: false, - status: 200, - }); - } - } catch (error) { - logger.error(error); - return NextResponse.json({ - message: LIVENESS_NO_MATCH_ERROR, - isMatch: false, - status: 500, - }); - } - } -} - -export async function POST( - req: NextRequest, - { params }: { params: { sessionId: string } }, - res: NextResponse, -): Promise { +export async function POST(req: NextRequest) { const client = await getRekognitionClient(req); - const response = await client.createFaceLivenessSession({ + const { SessionId: sessionId } = await client.createFaceLivenessSession({ // TODO: Create a unique token for each request, and reuse on retry // ClientRequestToken: req.cookies.token, }); - return NextResponse.json({ - sessionId: response.SessionId, - status: 200, - }); + + return NextResponse.json({ sessionId }); } diff --git a/src/app/api/citizens/[cedula]/route.ts b/src/app/api/citizens/[cedula]/route.ts index 8fbaf7ca..13e030d3 100644 --- a/src/app/api/citizens/[cedula]/route.ts +++ b/src/app/api/citizens/[cedula]/route.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from 'next/server'; -import axios from 'axios'; import { CitizensBasicInformationResponse, @@ -7,59 +6,37 @@ import { CitizensTokenResponse, } from '../../types'; import { CitizensDataFlow } from '../../types/citizens.type'; +import { unwrap } from '@/helpers'; export async function GET( req: NextRequest, - { params }: { params: { cedula: string } }, - res: NextResponse, -): Promise { - const http = axios.create({ - baseURL: process.env.CEDULA_API, - }); - const url = new URL(req.url); + { params: { cedula } }: Props, + res: NextResponse>, +) { + const baseURL = process.env.CEDULA_API!; + const apiKey = process.env.CEDULA_API_KEY!; - const { cedula } = params; - const validatedQueryParam = url.searchParams.get('validated'); - const validated = validatedQueryParam && validatedQueryParam === 'true'; + const headers = await fetchAuthHeaders(); - const { data: citizensToken } = await http.post( - `${process.env.CEDULA_TOKEN_API}`, - { - grant_type: 'client_credentials', - }, - { - headers: { - Authorization: `Basic ${process.env.CITIZENS_API_AUTH_KEY}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }, - ); + const citizenUrl = new URL(`${baseURL}/${cedula}/info/basic`); + citizenUrl.searchParams.append('api-key', apiKey); + const { payload: citizen } = await fetch(citizenUrl, { + headers, + }).then(unwrap); - const { data: citizen } = await http.get( - `/${cedula}/info/basic?api-key=${process.env.CEDULA_API_KEY}`, - { - headers: { - Authorization: `Bearer ${citizensToken.access_token}`, - }, - }, - ); + const { names, id, firstSurname, secondSurname, gender } = citizen; - const { names, id, firstSurname, secondSurname, gender } = citizen.payload; - const name = names.split(' ')[0]; + const validated = new URL(req.url).searchParams.get('validated') === 'true'; if (validated) { - const { data: citizensBirthData } = - await http.get( - `/${cedula}/info/birth?api-key=${process.env.CEDULA_API_KEY}`, - { - headers: { - Authorization: `Bearer ${citizensToken.access_token}`, - }, - }, - ); + const headers = await fetchAuthHeaders(); + const birthUrl = new URL(`${baseURL}/${cedula}/info/birth`); + birthUrl.searchParams.append('api-key', apiKey); + const { payload: birth } = await fetch(birthUrl, { + headers, + }).then(unwrap); - let { birthDate } = citizensBirthData.payload; - birthDate = birthDate.split('T')[0]; + const [birthDate] = birth.birthDate.split('T'); return NextResponse.json({ names, @@ -72,7 +49,23 @@ export async function GET( } return NextResponse.json({ - name, + name: names.split(' ')[0], id, }); } + +const fetchAuthHeaders = async () => + fetch(process.env.CEDULA_TOKEN_API!, { + method: 'POST', + body: 'grant_type=client_credentials', + headers: { + Authorization: `Basic ${process.env.CITIZENS_API_AUTH_KEY}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + .then(unwrap) + .then(({ access_token }) => ({ + Authorization: `Bearer ${access_token}`, + })); + +type Props = { params: { cedula: string } }; diff --git a/src/app/api/iam/[cedula]/route.ts b/src/app/api/iam/[cedula]/route.ts index c8dfc582..86cbab6c 100644 --- a/src/app/api/iam/[cedula]/route.ts +++ b/src/app/api/iam/[cedula]/route.ts @@ -1,31 +1,23 @@ import { NextRequest, NextResponse } from 'next/server'; -import axios from 'axios'; -import { Identity } from '../../types'; +import type { Identity } from '../../types'; +import { unwrap } from '@/helpers'; export const dynamicParams = true; -export async function GET( - req: NextRequest, - { params }: { params: { cedula: string } }, -): Promise { - const http = axios.create({ - baseURL: process.env.NEXT_PUBLIC_ORY_SDK_URL, +export async function GET(req: NextRequest, { params: { cedula } }: Props) { + const url = new URL('admin/identities', process.env.NEXT_PUBLIC_ORY_SDK_URL); + url.searchParams.append('credentials_identifier', cedula); + + const identity = await fetch(url, { headers: { - Authorization: 'Bearer ' + process.env.ORY_SDK_TOKEN, + Authorization: `Bearer ${process.env.ORY_SDK_TOKEN}`, }, - }); - - const cedula = params.cedula; - - const { data: identity } = await http.get( - `/admin/identities?credentials_identifier=${cedula}`, - ); - - const exists = identity.length !== 0; + }).then(unwrap); return NextResponse.json({ - exists: exists, - status: 200, + exists: identity.length !== 0, }); } + +type Props = { params: { cedula: string } }; diff --git a/src/app/api/pwned/[password]/route.ts b/src/app/api/pwned/[password]/route.ts index fe947b8a..875362d3 100644 --- a/src/app/api/pwned/[password]/route.ts +++ b/src/app/api/pwned/[password]/route.ts @@ -4,32 +4,37 @@ import { pwnedPassword } from 'hibp'; import { Crypto } from '@/helpers'; import logger from '@/lib/logger'; -export async function GET( - req: NextRequest, - params: { params: { password: string } }, - res: NextResponse, -): Promise { - const { password } = params.params; +export async function GET(req: NextRequest, { params: { password } }: Props) { + if (!password) { + return NextResponse.json( + { + error: intl.error.emptyPasswd, + }, + { status: 400 }, + ); + } - if (typeof password !== 'undefined') { - const passwordKey = Array.isArray(password) ? password[0] : password; + try { + const data = await pwnedPassword(Crypto.decrypt(password)); - try { - const data = await pwnedPassword(Crypto.decrypt(passwordKey)); - return NextResponse.json({ - data, - status: 200, - }); - } catch (error) { - logger.error('Decryption Error: ', error); + return NextResponse.json({ data }); + } catch (error) { + logger.error(intl.error.crypto, error); - return NextResponse.json({ - status: 500, - }); - } - } else { - return NextResponse.json({ - status: 400, - }); + return NextResponse.json( + { + error: intl.error.crypto, + }, + { status: 500 }, + ); } } + +type Props = { params: { password: string } }; + +const intl = { + error: { + emptyPasswd: "Password can't be empty", + crypto: 'Decryption Error:', + }, +}; diff --git a/src/app/api/recaptcha/route.ts b/src/app/api/recaptcha/route.ts index 32fccdd4..d2cefb20 100644 --- a/src/app/api/recaptcha/route.ts +++ b/src/app/api/recaptcha/route.ts @@ -1,84 +1,62 @@ -import { ReCaptchaResponse } from '../types'; -import axios, { AxiosResponse } from 'axios'; +import type { ReCaptchaResponse, ReCaptchaEvent } from '../types'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import logger from '@/lib/logger'; -type ReCaptchaEvent = { - event: { - token: string; - siteKey: string; - expectedAction: string; - }; -}; - -const verifyRecaptcha = async ( - recaptchaEvent: ReCaptchaEvent, -): Promise => { - const apiKey = process.env.RECAPTHA_API_KEY; +const verifyRecaptcha = async (recaptchaEvent: ReCaptchaEvent) => { const projectId = process.env.RECAPTHA_PROJECT_ID; - - const assesmentUrl = `https://recaptchaenterprise.googleapis.com/v1/projects/${projectId}/assessments?key=${apiKey}`; - const response: AxiosResponse = await axios.post( - assesmentUrl, - recaptchaEvent, + const assesment = new URL( + `https://recaptchaenterprise.googleapis.com/v1/projects/${projectId}/assessments`, ); + assesment.searchParams.append('key', process.env.RECAPTHA_API_KEY!); - return response.data; + const options: RequestInit = { + method: 'POST', + body: JSON.stringify(recaptchaEvent), + }; + + return fetch(assesment, options).then((res) => res.json()); }; -export async function POST(req: NextRequest, res: NextResponse) { - if (!process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY) { - throw new Error( - 'NEXT_PUBLIC_RECAPTCHA_SITE_KEY not found in environment variables', - ); +export async function POST(req: NextRequest) { + const siteKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY; + + if (!siteKey) { + throw new Error(`NEXT_PUBLIC_RECAPTCHA_SITE_KEY: ${intl.error.noEnv}`); } try { - const body = await req.json(); - const recaptchaEvent: ReCaptchaEvent = { + const { token }: { token: string } = await req.json(); + + // For more info check, https://developers.google.com/recaptcha/docs/v3 + const { riskAnalysis } = await verifyRecaptcha({ event: { - token: body.token, - siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY, + token, + siteKey, expectedAction: 'form_submit', }, - }; + }); - // Recaptcha response - const response = (await verifyRecaptcha( - recaptchaEvent, - )) as ReCaptchaResponse; + const isHuman = riskAnalysis && riskAnalysis.score >= 0.3; - // Checking if the reponse sent by reCaptcha success or not and if the score is above 0.5 - // In ReCaptcha v3, a score sent which tells if the data sent from front end is from Human or from Bots. If score above 0.5 then it is human otherwise it is bot - // For more info check, https://developers.google.com/recaptcha/docs/v3 - // ReCaptcha v3 response, { - // "success": true|false, // whether this request was a valid reCAPTCHA token for your site - // "score": number // the score for this request (0.0 - 1.0) - // "action": string // the action name for this request (important to verify) - // "challenge_ts": timestamp, // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ) - // "hostname": string, // the hostname of the site where the reCAPTCHA was solved - // "error-codes": [...] // optional - // } - if (response.riskAnalysis && response.riskAnalysis.score >= 0.3) { - return NextResponse.json({ - isHuman: true, - message: 'Thank you human', - status: 200, - }); - } else { - return NextResponse.json({ - isHuman: false, - message: 'Google ReCaptcha Failure', - status: 200, - }); - } + return NextResponse.json({ isHuman }); } catch (error) { - logger.error('Google ReCaptcha crashed', error); - NextResponse.json({ - error: 'Something went wrong, please try again.', - status: 500, - }); + logger.error(intl.error.reCaptcha, error); + + return NextResponse.json( + { + error: intl.error.server, + }, + { status: 500 }, + ); } } + +const intl = { + error: { + server: 'Something went wrong, please try again', + reCaptcha: 'Google ReCaptcha crashed', + noEnv: 'variable invalid or undefined', + }, +}; diff --git a/src/app/api/types/index.ts b/src/app/api/types/index.ts index 2d42d0d2..0e008e34 100644 --- a/src/app/api/types/index.ts +++ b/src/app/api/types/index.ts @@ -6,4 +6,4 @@ export type { CitizensBirthInformationResponse, } from './citizens.type'; export type { Identity } from './iam.type'; -export type { ReCaptchaResponse } from './recaptcha.type'; +export type { ReCaptchaResponse, ReCaptchaEvent } from './recaptcha.type'; diff --git a/src/app/api/types/recaptcha.type.ts b/src/app/api/types/recaptcha.type.ts index 0ccab179..234d2af5 100644 --- a/src/app/api/types/recaptcha.type.ts +++ b/src/app/api/types/recaptcha.type.ts @@ -16,3 +16,11 @@ export type ReCaptchaResponse = { }; name: string; }; + +export type ReCaptchaEvent = { + event: { + token: string; + siteKey: string; + expectedAction: string; + }; +}; diff --git a/src/app/register/stepper/index.tsx b/src/app/register/stepper/index.tsx index adf019c8..15a1acd9 100644 --- a/src/app/register/stepper/index.tsx +++ b/src/app/register/stepper/index.tsx @@ -16,6 +16,7 @@ import { routes } from '@/constants/routes'; import Step1 from './step1'; import Step2 from './step2'; import Step3 from './step3'; +import { CitizensDataFlow } from '@/app/api/types/citizens.type'; const steps = ['Identificación', 'Verificación', 'Registro']; const optionalLabels = [ @@ -30,7 +31,7 @@ export default function StepperRegister() { const matches = useMediaQuery(theme.breakpoints.up('sm')); const [activeStep, setActiveStep] = useState(0); const [skipped, setSkipped] = useState(new Set()); - const [infoCedula, setInfoCedula] = useState({}); + const [infoCedula, setInfoCedula] = useState({} as CitizensDataFlow); const handleNext = () => { if (activeStep === steps.length - 1) { diff --git a/src/app/register/stepper/step1.tsx b/src/app/register/stepper/step1.tsx index 1f412f31..d6c98016 100644 --- a/src/app/register/stepper/step1.tsx +++ b/src/app/register/stepper/step1.tsx @@ -6,7 +6,6 @@ import { useReCaptcha } from 'next-recaptcha-v3'; import { useForm } from 'react-hook-form'; import { IMaskInput } from 'react-imask'; import Link from 'next/link'; -import axios from 'axios'; import { IDENTITY_ALREADY_EXISTS_ERROR, @@ -18,11 +17,12 @@ import { import { CedulaInput, CustomProps } from '../../../common/interfaces'; import { GridContainer, GridItem } from '@/components/elements/grid'; import LoadingBackdrop from '@/components/elements/loadingBackdrop'; +import { CitizensDataFlow } from '@/app/api/types/citizens.type'; import { TextBodyTiny } from '@/components/elements/typography'; import { useSnackAlert } from '@/components/elements/alert'; import { cedulaSchema } from '../../../common/yup-schemas'; import { ButtonApp } from '@/components/elements/button'; -import { Validations } from '@/helpers'; +import { Validations, unwrap } from '@/helpers'; const TextMaskCustom = forwardRef( function TextMaskCustom(props, ref: any) { @@ -41,7 +41,7 @@ const TextMaskCustom = forwardRef( }, ); -export default function Step1({ setInfoCedula, handleNext }: any) { +export default function Step1({ setInfoCedula, handleNext }: Props) { const { AlertError, AlertWarning } = useSnackAlert(); const [loading, setLoading] = useState(false); const { executeRecaptcha } = useReCaptcha(); @@ -88,29 +88,27 @@ export default function Step1({ setInfoCedula, handleNext }: any) { } try { - const { - data: { isHuman }, - } = await axios.post<{ isHuman: boolean }>('/api/recaptcha', { - token, - }); + const { isHuman } = await fetch('/api/recaptcha', { + method: 'POST', + body: JSON.stringify({ token }), + }).then(unwrap); if (!isHuman) { return AlertError(RECAPTCHA_VALIDATION_ERROR); } - const { - data: { exists }, - } = await axios.get<{ exists: boolean }>(`/api/iam/${cleanCedula}`); + const { exists } = await fetch(`/api/iam/${cleanCedula}`).then( + unwrap, + ); if (exists) { return AlertError(IDENTITY_ALREADY_EXISTS_ERROR); } - const { data: citizen } = await axios.get( - `/api/citizens/${cleanCedula}`, - ); + await fetch(`/api/citizens/${cleanCedula}`) + .then(unwrap) + .then(setInfoCedula); - setInfoCedula(citizen); handleNext(); } catch (err: any) { console.error(err.message || err); @@ -187,3 +185,11 @@ export default function Step1({ setInfoCedula, handleNext }: any) { ); } + +type Props = { + setInfoCedula: (info: CitizensDataFlow) => void; + handleNext: () => void; +}; + +type Exists = { exists: boolean }; +type Human = { isHuman: boolean }; diff --git a/src/app/register/stepper/step3.tsx b/src/app/register/stepper/step3.tsx index a3586708..a8bd9d15 100644 --- a/src/app/register/stepper/step3.tsx +++ b/src/app/register/stepper/step3.tsx @@ -1,4 +1,5 @@ 'use client'; + import { Alert, Box, @@ -17,22 +18,21 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; -import axios from 'axios'; import { CREATE_BROWSER_REGISTRATION_FLOW_ERROR, CREATE_IDENTITY_ERROR, VALIDATE_PASSWORD_ERROR, } from '../../../constants'; -import { GridContainer, GridItem } from '@/components/elements/grid'; import PasswordLevel, { calculatePasswordStrength, } from '@/components/elements/passwordLevel'; +import { GridContainer, GridItem } from '@/components/elements/grid'; import { CitizenCompleteData, Step3Form } from '../../../common/interfaces'; import { useSnackAlert } from '@/components/elements/alert'; import { step3Schema } from '../../../common/yup-schemas'; import { ButtonApp } from '@/components/elements/button'; -import { Crypto } from '@/helpers'; +import { Crypto, unwrap } from '@/helpers'; import { ory } from '@/lib/ory'; export default function Step3({ handleNext, infoCedula }: any) { @@ -97,7 +97,7 @@ export default function Step3({ handleNext, infoCedula }: any) { const password = Crypto.encrypt(form.password); try { - const { data } = await axios.get(`/api/pwned/${password}`); + const data = await fetch(`/api/pwned/${password}`).then(unwrap); const isValidPassword = data !== 0; setIsPwned(isValidPassword); @@ -108,9 +108,9 @@ export default function Step3({ handleNext, infoCedula }: any) { } try { - const { data: citizen } = await axios.get( + const citizen = await fetch( `/api/citizens/${infoCedula.id}?validated=true`, - ); + ).then(unwrap); let csrfToken = ''; if (flow && flow.ui && Array.isArray(flow.ui.nodes)) { diff --git a/src/common/interfaces/step-2-props.interface.ts b/src/common/interfaces/step-2-props.interface.ts index ad98fb7d..b3bb2bba 100644 --- a/src/common/interfaces/step-2-props.interface.ts +++ b/src/common/interfaces/step-2-props.interface.ts @@ -1,5 +1,7 @@ +import type { CitizensDataFlow } from '@/app/api/types/citizens.type'; + export interface Step2Props { - infoCedula: { [key: string]: any }; + infoCedula: CitizensDataFlow; handleNext: () => void; handleReset: () => void; } diff --git a/src/components/biometric/face-liveness-detector.tsx b/src/components/biometric/face-liveness-detector.tsx index 488da63a..e9204c6e 100644 --- a/src/components/biometric/face-liveness-detector.tsx +++ b/src/components/biometric/face-liveness-detector.tsx @@ -12,23 +12,26 @@ import { displayText } from './displayText'; import { useSnackAlert } from '@/components/elements/alert'; import { UNIDENTIFIED_ERROR } from '@/constants'; +import { unwrap } from '@/helpers'; Amplify.configure(awsExports); -export function LivenessQuickStartReact({ handleNextForm, cedula }: any) { - const next = handleNextForm; - const id = cedula; +type Props = { handleNextForm: () => void; cedula: string }; + +export function LivenessQuickStartReact({ + handleNextForm: next, + cedula, +}: Props) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [sessionId, setSessionId] = useState(null); const { AlertError } = useSnackAlert(); const fetchCreateLiveness: () => Promise = async () => { - const response = await fetch(`/api/biometric`, { method: 'POST' }); - await new Promise((r) => setTimeout(r, 2000)); - const { sessionId } = await response.json(); + await fetch(`/api/biometric`, { method: 'POST' }) + .then(unwrap) + .then(({ sessionId }) => setSessionId(sessionId)); - setSessionId(sessionId); setLoading(false); }; @@ -38,12 +41,11 @@ export function LivenessQuickStartReact({ handleNextForm, cedula }: any) { }; const handleAnalysisComplete: () => Promise = async () => { - const response = await fetch( - `/api/biometric?sessionId=${sessionId}&cedula=${id}`, + const data = await fetch(`/api/biometric/${sessionId}/${cedula}`).then( + unwrap, ); - const data = await response.json(); - if (data.isMatch === true) { + if (data?.isMatch === true) { next(); } else { setError(data); diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 856addaf..b569be42 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -1,3 +1,4 @@ export { getRekognitionClient } from './rekognition'; export { Validations } from './validations'; export { Crypto } from './cryptr'; +export { unwrap } from './unwrap'; diff --git a/src/helpers/unwrap.ts b/src/helpers/unwrap.ts new file mode 100644 index 00000000..ceaf2796 --- /dev/null +++ b/src/helpers/unwrap.ts @@ -0,0 +1 @@ +export const unwrap = (r: Response) => r.json(); diff --git a/tsconfig.json b/tsconfig.json index 371e3277..230fa81b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -48,7 +48,8 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + "env.d.ts" ], "exclude": [ "node_modules" diff --git a/yarn.lock b/yarn.lock index 0a5f214a..df108c5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5218,15 +5218,6 @@ axios@^0.24.0: dependencies: follow-redirects "^1.14.4" -axios@^1.4.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" - integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ== - dependencies: - follow-redirects "^1.15.0" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - axobject-query@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -6539,7 +6530,7 @@ flatted@^3.2.7: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.0, follow-redirects@^1.14.4, follow-redirects@^1.14.8, follow-redirects@^1.15.0: +follow-redirects@^1.14.0, follow-redirects@^1.14.4, follow-redirects@^1.14.8: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -6565,15 +6556,6 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -8507,11 +8489,6 @@ protobufjs@^7.0.0, protobufjs@^7.2.4: "@types/node" ">=13.7.0" long "^5.0.0" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - psl@^1.1.28: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7"