Skip to content

Commit

Permalink
Merge branch 'main' into chore/app-migration-bot-firewall
Browse files Browse the repository at this point in the history
  • Loading branch information
JuroUhlar committed Sep 24, 2024
2 parents d15cda2 + 7831656 commit cd79a23
Show file tree
Hide file tree
Showing 14 changed files with 120 additions and 105 deletions.
13 changes: 7 additions & 6 deletions e2e/sms-pumping/bot-unprotected.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { expect, test } from '@playwright/test';
import { TEST_IDS } from '../../src/client/testIDs';
import {
MAX_SMS_ATTEMPTS,
SMS_ATTEMPT_TIMEOUT_MAP,
SMS_FRAUD_COPY,
TEST_PHONE_NUMBER,
} from '../../src/server/sms-pumping/smsPumpingConst';

import { assertAlert, assertSnackbar, blockGoogleTagManager, resetScenarios } from '../e2eTestUtils';
import { ONE_MINUTE_MS } from '../../src/shared/timeUtils';
import { TEST_BUILD } from '../../src/envShared';
import {
SMS_FRAUD_COPY,
TEST_PHONE_NUMBER,
MAX_SMS_ATTEMPTS,
SMS_ATTEMPT_TIMEOUT_MAP,
} from '../../src/app/sms-pumping/api/smsPumpingConst';

const TEST_ID = TEST_IDS.smsFraud;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState } from 'react';
'use client';

import { FunctionComponent, Suspense, useState } from 'react';
import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper';
import React from 'react';
import { USE_CASES } from '../../client/components/common/content';
Expand All @@ -9,13 +11,14 @@ import { CloseSnackbarButton } from '../../client/components/common/Alert/Alert'
import { enqueueSnackbar } from 'notistack';
import { useCopyToClipboard } from 'react-use';
import { BackArrow } from '../../client/components/common/BackArrow/BackArrow';
import { GetServerSideProps, NextPage } from 'next';
import { SubmitCodeForm } from '../../client/components/sms-pumping/SubmitCodeForm';
import { PhoneNumberForm } from '../../client/components/sms-pumping/PhoneNumberForm';
import { GetServerSideProps } from 'next';
import { SubmitCodeForm } from './components/SubmitCodeForm';
import { PhoneNumberForm } from './components/PhoneNumberForm';
import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react';
import { useMutation } from 'react-query';
import { SendSMSResponse, SendSMSPayload } from '../api/sms-pumping/send-verification-sms';
import { TEST_PHONE_NUMBER } from '../../server/sms-pumping/smsPumpingConst';
import { useSearchParams } from 'next/navigation';
import { SendSMSPayload, SendSMSResponse } from './api/send-verification-sms/route';
import { TEST_PHONE_NUMBER } from './api/smsPumpingConst';

type FormStep = 'Send SMS' | 'Submit code';
type QueryAsProps = {
Expand Down Expand Up @@ -50,7 +53,7 @@ export const useSendMessage = ({ onSuccess, disableBotDetection = false }: SendM
mutationKey: 'sendSms',
mutationFn: async ({ phoneNumber, email }) => {
const { requestId } = await getData();
const response = await fetch(`/api/sms-pumping/send-verification-sms`, {
const response = await fetch(`/sms-pumping/api/send-verification-sms`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -76,12 +79,16 @@ export const useSendMessage = ({ onSuccess, disableBotDetection = false }: SendM
});
};

const SmsFraudUseCase: NextPage<QueryAsProps> = ({ disableBotDetection }) => {
const SmsFraud: FunctionComponent = () => {
// Default mocked user data
const [phoneNumber, setPhoneNumber] = useState(TEST_PHONE_NUMBER);
const [email, setEmail] = useState('[email protected]');
const [formStep, setFormStep] = useState<FormStep>('Send SMS');

const searchParams = useSearchParams();
const disableBotDetection =
searchParams?.get('disableBotDetection') === '1' || searchParams?.get('disableBotDetection') === 'true';

const [, copyToClipboard] = useCopyToClipboard();
const sendMessageMutation = useSendMessage({
onSuccess: (data) => {
Expand Down Expand Up @@ -138,4 +145,11 @@ const SmsFraudUseCase: NextPage<QueryAsProps> = ({ disableBotDetection }) => {
);
};

export default SmsFraudUseCase;
export const SmsPumpingUseCase = () => {
// Suspense required due to useSearchParams() https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
return (
<Suspense>
<SmsFraud />
</Suspense>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sequelize } from '../server';
import { Attributes, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
import { sequelize } from '../../../server/server';

interface SmsVerificationAttributes
extends Model<InferAttributes<SmsVerificationAttributes>, InferCreationAttributes<SmsVerificationAttributes>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { Severity, getAndValidateFingerprintResult } from '../../../server/checks';
import { isValidPostRequest } from '../../../server/server';
import { RealSmsPerVisitorModel, SmsVerificationDatabaseModel } from '../../../server/sms-pumping/database';
import { ONE_SECOND_MS, readableMilliseconds } from '../../../shared/timeUtils';
import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks';
import { ONE_SECOND_MS, readableMilliseconds } from '../../../../shared/timeUtils';
import { Op } from 'sequelize';
import { pluralize } from '../../../shared/utils';
import { pluralize } from '../../../../shared/utils';
import Twilio from 'twilio';
import { hashString } from '../../../server/server-utils';
import { hashString } from '../../../../server/server-utils';
import { env } from '../../../../env';
import { NextRequest, NextResponse } from 'next/server';
import { RealSmsPerVisitorModel, SmsVerificationDatabaseModel } from '../database';
import {
SMS_ATTEMPT_TIMEOUT_MAP,
TEST_PHONE_NUMBER,
MAX_SMS_ATTEMPTS,
REAL_SMS_LIMIT_PER_VISITOR,
SMS_FRAUD_COPY,
TEST_PHONE_NUMBER,
} from '../../../server/sms-pumping/smsPumpingConst';
import { env } from '../../../env';
SMS_ATTEMPT_TIMEOUT_MAP,
REAL_SMS_LIMIT_PER_VISITOR,
} from '../smsPumpingConst';

export type SendSMSPayload = {
requestId: string;
Expand Down Expand Up @@ -84,15 +83,8 @@ const sendSms = async (phone: string, body: string, visitorId: string) => {
console.log('Message sent: ', message.sid);
};

export default async function sendVerificationSMS(req: NextApiRequest, res: NextApiResponse<SendSMSResponse>) {
// 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 { phoneNumber: phone, email, requestId, disableBotDetection } = req.body as SendSMSPayload;
export async function POST(req: NextRequest): Promise<NextResponse<SendSMSResponse>> {
const { phoneNumber: phone, email, requestId, disableBotDetection } = (await req.json()) as SendSMSPayload;

// Get the full identification Fingerprint Server API, check it authenticity and filter away Bot and Tor requests
const fingerprintResult = await getAndValidateFingerprintResult({
Expand All @@ -104,15 +96,13 @@ export default async function sendVerificationSMS(req: NextApiRequest, res: Next
},
});
if (!fingerprintResult.okay) {
res.status(403).send({ severity: 'error', message: fingerprintResult.error });
return;
return NextResponse.json({ message: fingerprintResult.error, severity: 'error' }, { status: 403 });
}

// If identification data is missing, return an error
const visitorId = fingerprintResult.data.products?.identification?.data?.visitorId;
if (!visitorId) {
res.status(403).send({ severity: 'error', message: 'Identification data not found.' });
return;
return NextResponse.json({ message: 'Identification data not found.', severity: 'error' }, { status: 403 });
}

// Retrieve SMS verification requests made by the same browser today from the database, most recent first
Expand All @@ -129,14 +119,14 @@ export default async function sendVerificationSMS(req: NextApiRequest, res: Next

// If there have been too many requests, shut the visitor down for the day
if (requestsToday >= MAX_SMS_ATTEMPTS) {
res.status(403).send({
severity: 'error',
message: SMS_FRAUD_COPY.blockedForToday({ requestsToday }),
data: {
remainingAttempts: 0,
return NextResponse.json(
{
message: SMS_FRAUD_COPY.blockedForToday({ requestsToday }),
severity: 'error',
data: { remainingAttempts: 0 },
},
});
return;
{ status: 403 },
);
}

// If the visitor already sent some requests recently, apply the appropriate cool-down period
Expand All @@ -145,22 +135,26 @@ export default async function sendVerificationSMS(req: NextApiRequest, res: Next
const timeOut = SMS_ATTEMPT_TIMEOUT_MAP[requestsToday].timeout;
if (millisecondsToSeconds(lastRequestTimeAgoMs) < millisecondsToSeconds(timeOut - TIMEOUT_TOLERANCE_MS)) {
const waitFor = timeOut - lastRequestTimeAgoMs;
res.status(403).send({
severity: 'error',
message: `${SMS_FRAUD_COPY.needToWait({ requestsToday })} Max allowed is ${MAX_SMS_ATTEMPTS}. Please wait ${readableMilliseconds(waitFor)} to send another one.`,
});
return;
return NextResponse.json(
{
severity: 'error',
message: `${SMS_FRAUD_COPY.needToWait({ requestsToday })} Max allowed is ${MAX_SMS_ATTEMPTS}. Please wait ${readableMilliseconds(waitFor)} to send another one.`,
},
{ status: 403 },
);
}
}

// Apply a hard limit on the number of real SMS messages that cannot be reset to prevent people from abusing the demo
const realSmsCount = (await RealSmsPerVisitorModel.findOne({ where: { visitorId } }))?.realMessagesCount ?? 0;
if (phone !== TEST_PHONE_NUMBER && realSmsCount >= REAL_SMS_LIMIT_PER_VISITOR) {
res.status(403).send({
severity: 'error',
message: `You hit the hard demo limit of ${pluralize(REAL_SMS_LIMIT_PER_VISITOR, 'real SMS messages')} per visitor ID, thanks for testing! This cannot be reset. Please use the simulated phone number ${TEST_PHONE_NUMBER} to continue exploring the demo.`,
});
return;
return NextResponse.json(
{
severity: 'error',
message: `You hit the hard demo limit of ${pluralize(REAL_SMS_LIMIT_PER_VISITOR, 'real SMS messages')} per visitor ID, thanks for testing! This cannot be reset. Please use the simulated phone number ${TEST_PHONE_NUMBER} to continue exploring the demo.`,
},
{ status: 403 },
);
}

const verificationCode = generateRandomSixDigitCode();
Expand All @@ -186,13 +180,13 @@ export default async function sendVerificationSMS(req: NextApiRequest, res: Next
});
} catch (error) {
console.error(error);
res
.status(500)
.send({ severity: 'error', message: `An error occurred while sending the verification SMS message: ${error}` });
return;
return NextResponse.json(
{ severity: 'error', message: `An error occurred while sending the verification SMS message: ${error}` },
{ status: 500 },
);
}

res.status(200).send({
return NextResponse.json({
severity: 'success',
message: SMS_FRAUD_COPY.messageSent({ phone, messagesLeft: MAX_SMS_ATTEMPTS - requestsToday - 1 }),
data: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TEST_BUILD } from '../../envShared';
import { ONE_SECOND_MS } from '../../shared/timeUtils';
import { pluralize } from '../../shared/utils';
import { TEST_BUILD } from '../../../envShared';
import { ONE_SECOND_MS } from '../../../shared/timeUtils';
import { pluralize } from '../../../shared/utils';

export const TEST_PHONE_NUMBER = '+1234567890';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { Severity, getAndValidateFingerprintResult } from '../../../server/checks';
import { isValidPostRequest } from '../../../server/server';
import { SmsVerificationDatabaseModel } from '../../../server/sms-pumping/database';
import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks';
import { Op } from 'sequelize';
import { hashString } from '../../../server/server-utils';
import { SMS_FRAUD_COPY } from '../../../server/sms-pumping/smsPumpingConst';
import { hashString } from '../../../../server/server-utils';
import { NextRequest, NextResponse } from 'next/server';
import { SmsVerificationDatabaseModel } from '../database';
import { SMS_FRAUD_COPY } from '../smsPumpingConst';

export type SubmitCodePayload = {
requestId: string;
Expand All @@ -17,28 +16,19 @@ export type SubmitCodeResponse = {
severity: Severity;
};

export default async function sendVerificationSMS(req: NextApiRequest, res: NextApiResponse<SubmitCodeResponse>) {
// 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 { phoneNumber, code, requestId } = req.body as SubmitCodePayload;
export async function POST(req: NextRequest): Promise<NextResponse<SubmitCodeResponse>> {
const { phoneNumber, code, requestId } = (await req.json()) as SubmitCodePayload;

// Get the full identification result from Fingerprint Server API and validate its authenticity
const fingerprintResult = await getAndValidateFingerprintResult({ requestId, req });
if (!fingerprintResult.okay) {
res.status(403).send({ severity: 'error', message: fingerprintResult.error });
return;
return NextResponse.json({ severity: 'error', message: fingerprintResult.error }, { status: 403 });
}

// If identification data is missing, return an error
const identification = fingerprintResult.data.products?.identification?.data;
if (!identification) {
res.status(403).send({ severity: 'error', message: 'Identification data not found.' });
return;
return NextResponse.json({ severity: 'error', message: 'Identification data not found.' }, { status: 403 });
}

// Retrieve SMS verification requests made by this browser to this phone number
Expand All @@ -55,16 +45,14 @@ export default async function sendVerificationSMS(req: NextApiRequest, res: Next

// If there are no SMS verification requests, return an error
if (!latestSmsVerificationRequest) {
res.status(403).send({ severity: 'error', message: 'No SMS verification requests found.' });
return;
return NextResponse.json({ severity: 'error', message: 'No SMS verification requests found.' }, { status: 403 });
}

// If the code is incorrect, return an error
if (latestSmsVerificationRequest.code !== code) {
res.status(403).send({ severity: 'error', message: SMS_FRAUD_COPY.incorrectCode });
return;
return NextResponse.json({ severity: 'error', message: SMS_FRAUD_COPY.incorrectCode }, { status: 403 });
}

// If the code is correct, return a success message
res.status(200).send({ severity: 'success', message: SMS_FRAUD_COPY.accountCreated });
return NextResponse.json({ severity: 'success', message: SMS_FRAUD_COPY.accountCreated });
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { FunctionComponent } from 'react';
import { TEST_IDS } from '../../testIDs';
import { TEST_IDS } from '../../../client/testIDs';
import { SendMessageButton } from './SendSMSMessageButton';
import classNames from 'classnames';
import styles from './smsPumping.module.scss';
import formStyles from '../../../styles/forms.module.scss';
import { SendMessageMutation } from '../../../pages/sms-pumping';
import { SendMessageMutation } from '../SmsPumping';

type PhoneNumberFormProps = {
phoneNumber: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ButtonHTMLAttributes, FunctionComponent } from 'react';
import { TEST_IDS } from '../../testIDs';
import { TEST_IDS } from '../../../client/testIDs';
import styles from './smsPumping.module.scss';
import { Alert } from '../common/Alert/Alert';
import Button from '../common/Button/Button';
import { SendMessageMutation } from '../../../pages/sms-pumping';
import { Alert } from '../../../client/components/common/Alert/Alert';
import Button from '../../../client/components/common/Button/Button';
import { SendMessageMutation } from '../SmsPumping';

type SendMessageButtonProps = {
sendMessageMutation: SendMessageMutation;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import classNames from 'classnames';
import { FunctionComponent, useState } from 'react';
import { useMutation } from 'react-query';
import { SubmitCodeResponse, SubmitCodePayload } from '../../../pages/api/sms-pumping/submit-code';
import { TEST_IDS } from '../../testIDs';
import { SubmitCodeResponse, SubmitCodePayload } from '../api/submit-code/route';
import { TEST_IDS } from '../../../client/testIDs';
import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react';
import { SendMessageButton } from './SendSMSMessageButton';
import { Alert } from '../common/Alert/Alert';
import Button from '../common/Button/Button';
import { Alert } from '../../../client/components/common/Alert/Alert';
import Button from '../../../client/components/common/Button/Button';
import styles from './smsPumping.module.scss';
import formStyles from '../../../styles/forms.module.scss';
import { SendMessageMutation } from '../../../pages/sms-pumping';
import { SendMessageMutation } from '../SmsPumping';

export const useSubmitCode = (params?: { onSuccess?: () => void }) => {
const { getData } = useVisitorData(
Expand All @@ -22,7 +22,7 @@ export const useSubmitCode = (params?: { onSuccess?: () => void }) => {
mutationKey: ['submitCode'],
mutationFn: async ({ code, phoneNumber }) => {
const { requestId } = await getData();
const response = await fetch(`/api/sms-pumping/submit-code`, {
const response = await fetch(`/sms-pumping/api/submit-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
9 changes: 9 additions & 0 deletions src/app/sms-pumping/embed/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { USE_CASES } from '../../../client/components/common/content';
import { generateUseCaseMetadata } from '../../../client/components/common/seo';
import { SmsPumpingUseCase } from '../SmsPumping';

export const metadata = generateUseCaseMetadata(USE_CASES.smsPumping);

export default function SmsPumping() {
return <SmsPumpingUseCase />;
}
9 changes: 9 additions & 0 deletions src/app/sms-pumping/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { USE_CASES } from '../../client/components/common/content';
import { generateUseCaseMetadata } from '../../client/components/common/seo';
import { SmsPumpingUseCase } from './SmsPumping';

export const metadata = generateUseCaseMetadata(USE_CASES.smsPumping);

export default function SmsPumping() {
return <SmsPumpingUseCase />;
}
Loading

0 comments on commit cd79a23

Please sign in to comment.