Skip to content

Commit

Permalink
feat: use case skeleton
Browse files Browse the repository at this point in the history
  • Loading branch information
JuroUhlar committed Mar 21, 2024
1 parent ee28724 commit f9f7530
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/client/testIDs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,8 @@ export const TEST_IDS = {
login: 'login',
password: 'password',
},
smsFraud: {
phoneNumber: 'phoneNumber',
submit: 'submit',
},
} as const;
57 changes: 57 additions & 0 deletions src/pages/api/sms-fraud/verify-number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { Severity } from '../../../server/checkResult';
import { getAndValidateFingerprintResult } from '../../../server/checks';
import { isValidPostRequest } from '../../../server/server';
import { saveBotVisit } from '../../../server/botd-firewall/botVisitDatabase';

export type SendSMSPayload = {
requestId: string;
phoneNumber: string;
disableBotDetection: boolean;
};

export type SendSMSResponse = {
message: string;
severity: Severity;
};

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, requestId, disableBotDetection } = req.body as SendSMSPayload;

// Get the full Identification and Bot Detection 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;
}

console.log('fingerprintResult', fingerprintResult);

const identification = fingerprintResult.data.products?.identification?.data;
const botData = fingerprintResult.data.products?.botd?.data;

// If a bot is detected, return an error
if (!disableBotDetection && botData?.bot?.result === 'bad') {
res.status(403).send({
severity: 'error',
message: '🤖 Malicious bot detected, SMS message was not sent.',
});
// Optionally, here you could also save the bot's IP address to a blocklist in your database
// and block all requests from this IP address in the future at a web server/firewall level.
saveBotVisit(botData, identification?.visitorId ?? 'N/A');
return;
}

// All checks passed, allow access
res.status(200).send({
severity: 'success',
message: `A verification SMS message was sent to ${phoneNumber}.`,
});
}
94 changes: 94 additions & 0 deletions src/pages/sms-verification-fraud/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useState } from 'react';
import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper';
import React from 'react';
import { USE_CASES } from '../../client/components/common/content';
import Button from '../../client/components/common/Button/Button';
import formStyles from '../../styles/forms.module.scss';
import classNames from 'classnames';
import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react';
import { TEST_IDS } from '../../client/testIDs';
import { useMutation } from 'react-query';
import { SendSMSResponse } from '../api/sms-fraud/verify-number';
import { Alert } from '../../client/components/common/Alert/Alert';

export default function Index() {
const { getData } = useVisitorData(
{ ignoreCache: true },
{
immediate: false,
},
);

// Default mocked user data
const [email, setEmail] = useState('[email protected]');
const [phoneNumber, setPhoneNumber] = useState('');

const {
mutate: sendVerificationSms,
data: sendSmsResponse,
isLoading,
} = useMutation<SendSMSResponse, Error>({
mutationKey: ['sendSms'],
mutationFn: async () => {
const { requestId } = await getData();
const response = await fetch(`/api/sms-fraud/verify-number`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phoneNumber,
requestId,
}),
});
if (response.status < 500) {
return await response.json();
} else {
throw new Error('Failed to send verification SMS: ' + response.statusText);
}
},
});

return (
<UseCaseWrapper useCase={USE_CASES.smsFraud}>
<div className={formStyles.wrapper}>
<form
onSubmit={(event) => {
event.preventDefault();
sendVerificationSms();
}}
className={classNames(formStyles.useCaseForm)}
>
<label>Email</label>
<input
type='text'
name='email'
placeholder='Email'
defaultValue={email}
onChange={(e) => setEmail(e.target.value)}
required
/>

<label>Phone number</label>
<span className={formStyles.description}>Use a international format without spaces like +441112223333</span>
<input
type='tel'
name='phone'
placeholder='Phone'
required
// Use international phone number format
pattern='[+][0-9]{1,3}[0-9]{9}'
defaultValue={phoneNumber}
data-testid={TEST_IDS.smsFraud.phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
/>

{sendSmsResponse ? <Alert severity={sendSmsResponse.severity}>{sendSmsResponse.message}</Alert> : null}
<Button disabled={isLoading} type='submit' data-testid={TEST_IDS.smsFraud.submit}>
{isLoading ? `Sending verification SMS...` : 'Create account'}
</Button>
</form>
</div>
</UseCaseWrapper>
);
}
Empty file.
8 changes: 7 additions & 1 deletion src/styles/forms.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ form.useCaseForm {
}

input[type='text'],
input[type='password'] {
input[type='password'],
input[type='tel'] {
padding: rem(16px) rem(12px);
border-radius: rem(6px);
border: 1px solid v('gray-box-stroke');
Expand Down Expand Up @@ -84,4 +85,9 @@ form.useCaseForm {
border-top: 1px solid v('gray-box-stroke');
margin: 0;
}

.description {
color: v('dark-gray');
font-size: rem(14px);
}
}

0 comments on commit f9f7530

Please sign in to comment.