Skip to content

Commit

Permalink
refactor: use fetch instead of axios (#99)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
JeffreyArt1 and marluanespiritusanto authored Oct 10, 2023
1 parent bc40fb5 commit 0861584
Show file tree
Hide file tree
Showing 19 changed files with 297 additions and 314 deletions.
19 changes: 19 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
95 changes: 95 additions & 0 deletions src/app/api/biometric/[sessionId]/[cedula]/route.ts
Original file line number Diff line number Diff line change
@@ -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());
};
105 changes: 4 additions & 101 deletions src/app/api/biometric/route.ts
Original file line number Diff line number Diff line change
@@ -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<any | void>,
): Promise<any> {
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<any | void>,
): Promise<any> {
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 });
}
83 changes: 38 additions & 45 deletions src/app/api/citizens/[cedula]/route.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios';

import {
CitizensBasicInformationResponse,
CitizensBirthInformationResponse,
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<CitizensDataFlow | void>,
): Promise<NextResponse> {
const http = axios.create({
baseURL: process.env.CEDULA_API,
});
const url = new URL(req.url);
{ params: { cedula } }: Props,
res: NextResponse<CitizensDataFlow | Pick<CitizensDataFlow, 'id' | 'name'>>,
) {
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<CitizensTokenResponse>(
`${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<CitizensBasicInformationResponse>(unwrap);

const { data: citizen } = await http.get<CitizensBasicInformationResponse>(
`/${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<CitizensBirthInformationResponse>(
`/${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<CitizensBirthInformationResponse>(unwrap);

let { birthDate } = citizensBirthData.payload;
birthDate = birthDate.split('T')[0];
const [birthDate] = birth.birthDate.split('T');

return NextResponse.json({
names,
Expand All @@ -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<CitizensTokenResponse>(unwrap)
.then(({ access_token }) => ({
Authorization: `Bearer ${access_token}`,
}));

type Props = { params: { cedula: string } };
32 changes: 12 additions & 20 deletions src/app/api/iam/[cedula]/route.ts
Original file line number Diff line number Diff line change
@@ -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<NextResponse> {
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<Identity[]>(
`/admin/identities?credentials_identifier=${cedula}`,
);

const exists = identity.length !== 0;
}).then<Identity[]>(unwrap);

return NextResponse.json({
exists: exists,
status: 200,
exists: identity.length !== 0,
});
}

type Props = { params: { cedula: string } };
Loading

0 comments on commit 0861584

Please sign in to comment.