Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: nextjs 13 migration #82

Merged
merged 21 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
454dc2c
refactor: nextjs 13 migration
gustavovalverde Aug 18, 2023
5b71ccc
Merge branch 'master' into refactor-nextjs-13
gustavovalverde Aug 18, 2023
62656f7
fix: nextjs13 migrations
gustavovalverde Aug 24, 2023
3c11e17
fix(build): use correct reference to variable
gustavovalverde Aug 24, 2023
7302e79
fix(styles): use MUI recommended Nextjs13 approach
gustavovalverde Aug 31, 2023
d32d412
fix(linter): add missing definitions from plugins
gustavovalverde Aug 31, 2023
e375db6
fix(ci): allow building with latest node
gustavovalverde Aug 31, 2023
2dce6ce
fix(linter): use general eslint disable rule
gustavovalverde Aug 31, 2023
2a10f69
fix: tests
gustavovalverde Aug 31, 2023
3f34c3b
imp(build): enable `yarn` cache
gustavovalverde Aug 31, 2023
f4994d3
build(docker): use correct `$PORT` and `$HOSTNAME` handling
gustavovalverde Aug 31, 2023
133d7e6
fix(deploy): use `0.0.0.0` as hostname
gustavovalverde Aug 31, 2023
8f14c1b
fix(build): use a newer Nextjs version to fix route handlers
gustavovalverde Sep 1, 2023
c9bbb4d
build(deps): update app dependencies
JeffreyArt1 Sep 1, 2023
bc9221c
fix(styles): loader positioning
JeffreyArt1 Sep 2, 2023
1bf64b2
fix(ux): use numeric input for cedula field
JeffreyArt1 Sep 2, 2023
c4bec7d
feat: edge-csrf middleware applied
marluanespiritusanto Sep 5, 2023
ecec6cf
Merge branch 'refactor-nextjs-13' of https://github.com/opticrd/cuent…
JeffreyArt1 Sep 6, 2023
d874e26
imp(csrf): just use type
JeffreyArt1 Sep 6, 2023
a7ff126
feat(core): cookie middleware implementation
marluanespiritusanto Sep 7, 2023
d8f30c1
fix(api): getting `validated` query params
marluanespiritusanto Sep 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ RECAPTHA_API_KEY=
RECAPTHA_PROJECT_ID=
SITE_COOKIE_KEY=
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=
NEXT_PUBLIC_GOOGLE_ANALYTICS=
NEXT_PUBLIC_GTM_ID=
CEDULA_TOKEN_API=
CITIZENS_API_AUTH_KEY=
ORY_SDK_URL=
NEXT_PUBLIC_ORY_SDK_URL=
ORY_SDK_TOKEN=
3 changes: 2 additions & 1 deletion .github/workflows/build-docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ jobs:
push: true
build-args: |
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=${{ secrets.NEXT_PUBLIC_RECAPTCHA_SITE_KEY }}
NEXT_PUBLIC_GOOGLE_ANALYTICS=${{ secrets.NEXT_PUBLIC_GOOGLE_ANALYTICS }}
NEXT_PUBLIC_GTM_ID=${{ secrets.NEXT_PUBLIC_GOOGLE_ANALYTICS }}
NEXT_PUBLIC_ORY_SDK_URL=${{ vars.NEXT_PUBLIC_ORY_SDK_URL }}
secrets: |
"AWS_EXPORTS_JSON=${{ secrets.AWS_EXPORTS_JSON }}"

Expand Down
1 change: 0 additions & 1 deletion .github/workflows/cloudrun-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ jobs:
service: ${{ env.GITHUB_REPOSITORY_NAME_PART_SLUG }}-${{ needs.versioning.outputs.version || env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
region: ${{ inputs.region }}
env_vars: |
ORY_SDK_URL=${{ secrets.ORY_SDK_URL }},
ORY_SDK_TOKEN=${{ secrets.ORY_SDK_TOKEN }},
CEDULA_API=${{ secrets.CEDULA_API }},
CEDULA_API_KEY=${{ secrets.CEDULA_API_KEY }},
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ env:
jobs:
test:
name: Test with Node.js ${{ matrix.node }}
timeout-minutes: 5
timeout-minutes: 10
runs-on: ubuntu-latest

strategy:
Expand All @@ -61,6 +61,7 @@ jobs:
uses: actions/setup-node@v3.7.0
with:
node-version: ${{ matrix.node }}
cache: yarn

- name: Audit for vulnerabilities
run: npx audit-ci@^6 --config ./audit-ci.jsonc
Expand Down
12 changes: 9 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS base
ARG WORK_DIR
ARG APP_ENV=production

ENV PORT=3000
ARG PORT=3000
ENV WORK_DIR=${WORK_DIR}
ENV NODE_ENV=${APP_ENV}
ENV NEXT_TELEMETRY_DISABLED=1
Expand All @@ -20,8 +20,11 @@ WORKDIR ${WORK_DIR}
ARG NEXT_PUBLIC_RECAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_RECAPTCHA_SITE_KEY=${NEXT_PUBLIC_RECAPTCHA_SITE_KEY}

ARG NEXT_PUBLIC_GOOGLE_ANALYTICS
ENV NEXT_PUBLIC_GOOGLE_ANALYTICS=${NEXT_PUBLIC_GOOGLE_ANALYTICS}
ARG NEXT_PUBLIC_GTM_ID
ENV NEXT_PUBLIC_GTM_ID=${NEXT_PUBLIC_GTM_ID}

ARG NEXT_PUBLIC_ORY_SDK_URL
ENV NEXT_PUBLIC_ORY_SDK_URL=${NEXT_PUBLIC_ORY_SDK_URL}

# ===================== Install Deps =====================
FROM base as deps
Expand Down Expand Up @@ -60,4 +63,7 @@ USER nextjs

EXPOSE ${PORT}

ENV PORT ${PORT}
ENV HOSTNAME 0.0.0.0

CMD ["node", "server.js"]
14 changes: 14 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
reactStrictMode: true,
output: 'standalone',
webpack: (config, { webpack, isServer, nextRuntime }) => {
// Avoid AWS SDK Node.js require issue
if (isServer && nextRuntime === 'nodejs')
config.plugins.push(
new webpack.IgnorePlugin({ resourceRegExp: /^aws-crt$/ }),
);
if (!isServer) {
config.externals = ['dtrace-provider'];
}
return config;
},
};

module.exports = nextConfig;
27 changes: 15 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,52 +23,55 @@
"dependencies": {
"@aws-amplify/ui-react-liveness": "^2.0.1",
"@aws-sdk/client-rekognition": "^3.379.1",
"@babel/core": "7.22.9",
"@babel/core": "^7.22.11",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.10.6",
"@google-cloud/logging-bunyan": "^4.2.2",
"@hookform/resolvers": "3.1.1",
"@mui/icons-material": "^5.14.1",
"@mui/material": "^5.14.2",
"@google-cloud/logging-bunyan": "^5.0.0",
"@hookform/resolvers": "^3.3.1",
"@mui/icons-material": "^5.14.3",
"@mui/material": "^5.14.5",
"@ory/client": "^1.1.41",
"@ory/integrations": "^1.1.4",
"@thgh/next-gtm": "^0.1.4",
"@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",
"cookie": "^0.5.0",
"cryptr": "^6.2.0",
"eslint": "^8.46.0",
"eslint-config-next": "13.4.12",
"eslint-config-next": "^13.4.19",
"hibp": "^13.0.0",
"next": "13.4.12",
"next": "13.4.19",
"next-recaptcha-v3": "^1.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-gtm-module": "^2.0.11",
"react-hook-form": "7.45.2",
"react-hook-form": "7.45.4",
"react-imask": "^7.1.3",
"sharp": "^0.32.4",
"source-map-support": "^0.5.21",
"typescript": "5.1.6",
"yup": "^1.2.0"
},
"devDependencies": {
"@types/cookie": "^0.5.1",
"@types/node": "^18.17.0",
"@types/node": "^20.5.7",
"@types/react": "^18.2.17",
"@types/react-dom": "^18.2.7",
"@types/react-google-recaptcha": "^2.1.5",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"eslint-config-prettier": "^8.9.0",
"aws-crt": "^1.18.0",
"encoding": "^0.1.13",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.3",
"install-peers": "^1.0.4",
"lint-staged": "^13.2.3",
"lint-staged": "^14.0.0",
"prettier": ">=3.0.0"
}
}
1 change: 1 addition & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { setCookie } from './set-cookie';
15 changes: 15 additions & 0 deletions src/actions/set-cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use server';

import { cookies } from 'next/headers';

export async function setCookie() {
const key = process.env.SITE_COOKIE_KEY as string;

cookies().set('token', key, {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 10,
sameSite: 'strict',
path: '/',
});
}
111 changes: 111 additions & 0 deletions src/app/api/biometric/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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> {
const client = await getRekognitionClient(req);

const response = 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,
});
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
import { NextApiRequest, NextApiResponse } from 'next/types';
import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios';

import {
CitizensBasicInformationResponse,
CitizensBirthInformationResponse,
CitizensTokenResponse,
} from '../types';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<{
id: string;
name?: string;
names?: string;
firstSurname?: string;
secondSurname?: string;
gender?: string;
birthDate?: string;
} | void>,
): Promise<void> {
const { token } = req.cookies;

if (token !== process.env.SITE_COOKIE_KEY) {
return res.status(401).send();
}
} from '../../types';
import { CitizensDataFlow } from '../../types/citizens.type';

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);

const { cedula, validated } = req.query;
const { cedula } = params;
const validatedQueryParam = url.searchParams.get('validated');
const validated = validatedQueryParam && validatedQueryParam === 'true';

const { data: citizensToken } = await http.post<CitizensTokenResponse>(
`${process.env.CEDULA_TOKEN_API}`,
Expand Down Expand Up @@ -70,7 +61,7 @@ export default async function handler(
let { birthDate } = citizensBirthData.payload;
birthDate = birthDate.split('T')[0];

return res.status(200).json({
return NextResponse.json({
names,
id,
firstSurname,
Expand All @@ -80,5 +71,8 @@ export default async function handler(
});
}

return res.status(200).json({ name, id });
return NextResponse.json({
name,
id,
});
}
31 changes: 31 additions & 0 deletions src/app/api/iam/[cedula]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios';

import { Identity } from '../../types';

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,
headers: {
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;

return NextResponse.json({
exists: exists,
status: 200,
});
}
Loading