From c2dd3ff6bc15bea1899e0434521d7e50fea20903 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Thu, 19 Sep 2024 18:03:05 +0100 Subject: [PATCH] Chore: Migrate credential stuffing to `app` INTER-911 (#158) * chore: move credential stuffing client to `app` * chore: move credential stuffing server to `app` --- .../CredentialStuffing.tsx} | 8 ++-- .../api/authenticate/route.ts} | 46 ++++++++----------- .../credentialStuffing.module.scss | 0 src/app/credential-stuffing/embed/page.tsx | 9 ++++ .../credential-stuffing/iconHidden.svg | 0 .../credential-stuffing/iconShown.svg | 0 src/app/credential-stuffing/page.tsx | 9 ++++ src/pages/credential-stuffing/embed.tsx | 13 ------ 8 files changed, 43 insertions(+), 42 deletions(-) rename src/{pages/credential-stuffing/index.tsx => app/credential-stuffing/CredentialStuffing.tsx} (94%) rename src/{pages/api/credential-stuffing/authenticate.ts => app/credential-stuffing/api/authenticate/route.ts} (70%) rename src/{pages => app}/credential-stuffing/credentialStuffing.module.scss (100%) create mode 100644 src/app/credential-stuffing/embed/page.tsx rename src/{pages => app}/credential-stuffing/iconHidden.svg (100%) rename src/{pages => app}/credential-stuffing/iconShown.svg (100%) create mode 100644 src/app/credential-stuffing/page.tsx delete mode 100644 src/pages/credential-stuffing/embed.tsx diff --git a/src/pages/credential-stuffing/index.tsx b/src/app/credential-stuffing/CredentialStuffing.tsx similarity index 94% rename from src/pages/credential-stuffing/index.tsx rename to src/app/credential-stuffing/CredentialStuffing.tsx index 2a1e8414..dd15fc0c 100644 --- a/src/pages/credential-stuffing/index.tsx +++ b/src/app/credential-stuffing/CredentialStuffing.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useState } from 'react'; import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper'; import React from 'react'; @@ -12,10 +14,10 @@ import shownIcon from './iconShown.svg'; import Image from 'next/image'; import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react'; import { TEST_IDS } from '../../client/testIDs'; -import { LoginPayload, LoginResponse } from '../api/credential-stuffing/authenticate'; import { useMutation } from 'react-query'; +import { LoginPayload, LoginResponse } from './api/authenticate/route'; -export default function Index() { +export function CredentialStuffing() { const { getData: getVisitorData } = useVisitorData( { ignoreCache: true }, { @@ -32,7 +34,7 @@ export default function Index() { mutationKey: ['login attempt'], mutationFn: async ({ username, password }) => { const { requestId, visitorId } = await getVisitorData({ ignoreCache: true }); - const response = await fetch('/api/credential-stuffing/authenticate', { + const response = await fetch('/credential-stuffing/api/authenticate', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/pages/api/credential-stuffing/authenticate.ts b/src/app/credential-stuffing/api/authenticate/route.ts similarity index 70% rename from src/pages/api/credential-stuffing/authenticate.ts rename to src/app/credential-stuffing/api/authenticate/route.ts index 948a5a12..32d4ffe6 100644 --- a/src/pages/api/credential-stuffing/authenticate.ts +++ b/src/app/credential-stuffing/api/authenticate/route.ts @@ -1,10 +1,10 @@ import { Op } from 'sequelize'; -import { isValidPostRequest, sequelize } from '../../../server/server'; -import { Severity, getAndValidateFingerprintResult } from '../../../server/checks'; -import { NextApiRequest, NextApiResponse } from 'next'; -import { CREDENTIAL_STUFFING_COPY } from '../../../server/credentialStuffing/copy'; -import { env } from '../../../env'; -import { LoginAttemptDbModel, LoginAttemptResult } from '../../../server/credentialStuffing/database'; +import { NextResponse } from 'next/server'; +import { env } from 'process'; +import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks'; +import { CREDENTIAL_STUFFING_COPY } from '../../../../server/credentialStuffing/copy'; +import { LoginAttemptResult, LoginAttemptDbModel } from '../../../../server/credentialStuffing/database'; +import { sequelize } from '../../../../server/server'; export type LoginPayload = { username: string; @@ -32,30 +32,21 @@ function getKnownVisitorIds() { return visitorIdsFromEnv ? [...defaultVisitorIds, ...visitorIdsFromEnv] : defaultVisitorIds; } -export default async function loginHandler(req: NextApiRequest, res: NextApiResponse) { - // This API route accepts only POST requests. - const reqValidation = isValidPostRequest(req); - if (!reqValidation.okay) { - res.status(405).send({ severity: 'error', message: reqValidation.error }); - return; - } - - const { requestId, username, password, visitorId: clientVisitorId } = req.body as LoginPayload; +export async function POST(req: Request): Promise> { + const { requestId, username, password, visitorId: clientVisitorId } = (await req.json()) as LoginPayload; // Get the full Identification result from Fingerprint Server API and validate its authenticity const fingerprintResult = await getAndValidateFingerprintResult({ requestId, req }); if (!fingerprintResult.okay) { logLoginAttempt(clientVisitorId, username, 'RequestIdValidationFailed'); - res.status(403).send({ severity: 'error', message: fingerprintResult.error }); - return; + return NextResponse.json({ message: fingerprintResult.error, severity: 'error' }, { status: 403 }); } // Get visitorId from the Server API Identification event const visitorId = fingerprintResult.data.products?.identification?.data?.visitorId; if (!visitorId) { logLoginAttempt(clientVisitorId, username, 'RequestIdValidationFailed'); - res.status(403).send({ severity: 'error', message: 'Visitor ID not found.' }); - return; + return NextResponse.json({ message: 'Visitor ID not found.', severity: 'error' }, { status: 403 }); } // If the visitor ID performed 5 unsuccessful login attempts during the last 24 hours we do not perform the login. @@ -73,28 +64,31 @@ export default async function loginHandler(req: NextApiRequest, res: NextApiResp }); if (failedLoginsToday.count >= 5) { logLoginAttempt(visitorId, username, 'TooManyLoginAttempts'); - res.status(403).send({ severity: 'error', message: CREDENTIAL_STUFFING_COPY.tooManyAttempts }); - return; + return NextResponse.json({ message: CREDENTIAL_STUFFING_COPY.tooManyAttempts, severity: 'error' }, { status: 403 }); } // If the provided credentials are incorrect, we return an error. if (!credentialsAreCorrect(username, password)) { logLoginAttempt(visitorId, username, 'IncorrectCredentials'); - res.status(403).send({ severity: 'error', message: CREDENTIAL_STUFFING_COPY.invalidCredentials }); - return; + return NextResponse.json( + { message: CREDENTIAL_STUFFING_COPY.invalidCredentials, severity: 'error' }, + { status: 403 }, + ); } // If the provided credentials are correct but the user never logged in using this browser, // we force the user to use multi-factor authentication (text message, email, authenticator app, etc.) if (!mockedUser.knownVisitorIds.includes(visitorId)) { logLoginAttempt(visitorId, username, 'UnknownBrowserEnforceMFA'); - res.status(403).send({ severity: 'warning', message: CREDENTIAL_STUFFING_COPY.differentVisitorIdUseMFA }); - return; + return NextResponse.json( + { message: CREDENTIAL_STUFFING_COPY.differentVisitorIdUseMFA, severity: 'warning' }, + { status: 403 }, + ); } // If the provided credentials are correct and we recognize the browser, we log the user in logLoginAttempt(visitorId, username, 'Success'); - res.status(200).send({ severity: 'success', message: CREDENTIAL_STUFFING_COPY.success }); + return NextResponse.json({ message: CREDENTIAL_STUFFING_COPY.success, severity: 'success' }); } // Dummy action simulating authentication. diff --git a/src/pages/credential-stuffing/credentialStuffing.module.scss b/src/app/credential-stuffing/credentialStuffing.module.scss similarity index 100% rename from src/pages/credential-stuffing/credentialStuffing.module.scss rename to src/app/credential-stuffing/credentialStuffing.module.scss diff --git a/src/app/credential-stuffing/embed/page.tsx b/src/app/credential-stuffing/embed/page.tsx new file mode 100644 index 00000000..4d870bcc --- /dev/null +++ b/src/app/credential-stuffing/embed/page.tsx @@ -0,0 +1,9 @@ +import { USE_CASES } from '../../../client/components/common/content'; +import { generateUseCaseMetadata } from '../../../client/components/common/seo'; +import { CredentialStuffing } from '.././CredentialStuffing'; + +export const metadata = generateUseCaseMetadata(USE_CASES.credentialStuffing); + +export default function CredentialStuffingPage() { + return ; +} diff --git a/src/pages/credential-stuffing/iconHidden.svg b/src/app/credential-stuffing/iconHidden.svg similarity index 100% rename from src/pages/credential-stuffing/iconHidden.svg rename to src/app/credential-stuffing/iconHidden.svg diff --git a/src/pages/credential-stuffing/iconShown.svg b/src/app/credential-stuffing/iconShown.svg similarity index 100% rename from src/pages/credential-stuffing/iconShown.svg rename to src/app/credential-stuffing/iconShown.svg diff --git a/src/app/credential-stuffing/page.tsx b/src/app/credential-stuffing/page.tsx new file mode 100644 index 00000000..8fec591e --- /dev/null +++ b/src/app/credential-stuffing/page.tsx @@ -0,0 +1,9 @@ +import { USE_CASES } from '../../client/components/common/content'; +import { generateUseCaseMetadata } from '../../client/components/common/seo'; +import { CredentialStuffing } from './CredentialStuffing'; + +export const metadata = generateUseCaseMetadata(USE_CASES.credentialStuffing); + +export default function CredentialStuffingPage() { + return ; +} diff --git a/src/pages/credential-stuffing/embed.tsx b/src/pages/credential-stuffing/embed.tsx deleted file mode 100644 index 71474224..00000000 --- a/src/pages/credential-stuffing/embed.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import CredentialStuffing from '.'; -import { GetStaticProps } from 'next'; -import { CustomPageProps } from '../_app'; - -export default CredentialStuffing; - -export const getStaticProps: GetStaticProps = async () => { - return { - props: { - embed: true, - }, - }; -};