Skip to content

Commit

Permalink
Chore: Migrate Coupon fraud demo to the app router INTER-911 (#157)
Browse files Browse the repository at this point in the history
* chore: move client code to app router

* chore: move server code to app router

* chore: remove needless import

* chore: fix build
  • Loading branch information
JuroUhlar authored Sep 19, 2024
1 parent bbfd7bb commit 2009679
Show file tree
Hide file tree
Showing 16 changed files with 62 additions and 70 deletions.
4 changes: 4 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ module.exports = {
includePaths: [path.join(__dirname, 'src/styles')],
prependData: `@import "common.scss";`,
},
experimental: {
// Necessary to prevent https://github.com/sequelize/sequelize/issues/16589
serverComponentsExternalPackages: ['sequelize'],
},
async headers() {
return [
{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"react-syntax-highlighter": "^15.5.0",
"react-use": "^17.5.0",
"react18-json-view": "^0.2.9-canary.0",
"sequelize": "^6.37.1",
"sequelize": "^6.37.3",
"sharp": "^0.33.2",
"twilio": "^5.0.1"
},
Expand Down
4 changes: 3 additions & 1 deletion src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { QueryClient, QueryClientProvider } from 'react-query';
import { SnackbarProvider } from 'notistack';
import { PropsWithChildren } from 'react';
import { CloseSnackbarButton, CustomSnackbar } from './client/components/common/Alert/Alert';
import { FpjsProvider } from '@fingerprintjs/fingerprintjs-pro-react';
import { FP_LOAD_OPTIONS } from './pages/_app';

const queryClient = new QueryClient({
defaultOptions: {
Expand Down Expand Up @@ -32,7 +34,7 @@ function Providers({ children }: PropsWithChildren) {
info: CustomSnackbar,
}}
>
{children}
<FpjsProvider loadOptions={FP_LOAD_OPTIONS}>{children}</FpjsProvider>
</SnackbarProvider>
</QueryClientProvider>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use client';

import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper';
import { useState } from 'react';
import React from 'react';
import { USE_CASES } from '../../client/components/common/content';
import { CustomPageProps } from '../_app';
import styles from './couponFraud.module.scss';
import formStyles from '../../styles/forms.module.scss';
import classNames from 'classnames';
Expand All @@ -14,13 +15,13 @@ import { Cart } from '../../client/components/common/Cart/Cart';
import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react';
import { TEST_IDS } from '../../client/testIDs';
import { useMutation } from 'react-query';
import { CouponClaimPayload, CouponClaimResponse } from '../api/coupon-fraud/claim';
import { CouponClaimPayload, CouponClaimResponse } from './api/claim/route';

const AIRMAX_PRICE = 356.02;
const ALLSTAR_PRICE = 102.5;
const TAXES = 6;

export default function CouponFraudUseCase({ embed }: CustomPageProps) {
export function CouponFraudUseCase() {
const { getData: getVisitorData } = useVisitorData(
{
ignoreCache: true,
Expand All @@ -36,7 +37,7 @@ export default function CouponFraudUseCase({ embed }: CustomPageProps) {
mutationKey: ['request coupon claim'],
mutationFn: async ({ couponCode }: Omit<CouponClaimPayload, 'requestId'>) => {
const { requestId } = await getVisitorData({ ignoreCache: true });
const response = await fetch('/api/coupon-fraud/claim', {
const response = await fetch('/coupon-fraud/api/claim', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -82,7 +83,7 @@ export default function CouponFraudUseCase({ embed }: CustomPageProps) {
];

return (
<UseCaseWrapper useCase={USE_CASES.couponFraud} embed={embed}>
<UseCaseWrapper useCase={USE_CASES.couponFraud}>
<div className={classNames(styles.wrapper, formStyles.wrapper)}>
<Cart items={cartItems} discount={discount} taxPerItem={TAXES} discountLabel='Coupon discount'></Cart>
<div className={styles.innerWrapper}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { isValidPostRequest } from '../../../server/server';
import { Op } from 'sequelize';
import { COUPON_CODES, CouponClaimDbModel, CouponCodeString } from '../../../server/coupon-fraud/database';
import { Severity, getAndValidateFingerprintResult } from '../../../server/checks';
import { NextApiRequest, NextApiResponse } from 'next';
import { COUPON_FRAUD_COPY } from '../../../server/coupon-fraud/copy';
import { COUPON_CODES, CouponClaimDbModel, CouponCodeString } from '../../../../server/coupon-fraud/database';
import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks';
import { COUPON_FRAUD_COPY } from '../../../../server/coupon-fraud/copy';
import { NextResponse } from 'next/server';

export type CouponClaimPayload = {
couponCode: string;
Expand All @@ -19,43 +18,32 @@ const isCouponCode = (couponCode: string): couponCode is CouponCodeString => {
return COUPON_CODES.includes(couponCode as CouponCodeString);
};

export default async function claimCouponHandler(req: NextApiRequest, res: NextApiResponse<CouponClaimResponse>) {
// 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 { couponCode, requestId } = req.body as CouponClaimPayload;
export async function POST(req: Request): Promise<NextResponse<CouponClaimResponse>> {
const { couponCode, requestId } = (await req.json()) as CouponClaimPayload;

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

// Get visitorId from the Server API Identification event
const visitorId = fingerprintResult.data.products?.identification?.data?.visitorId;
if (!visitorId) {
res.status(403).send({ severity: 'error', message: 'Visitor ID not found.' });
return;
return NextResponse.json({ severity: 'error', message: 'Visitor ID not found.' }, { status: 403 });
}

// Check if Coupon exists
if (!isCouponCode(couponCode)) {
res.status(403).send({ severity: 'error', message: COUPON_FRAUD_COPY.doesNotExist });
return;
return NextResponse.json({ severity: 'error', message: COUPON_FRAUD_COPY.doesNotExist }, { status: 403 });
}

// Check if visitor used this coupon before
const usedBefore = await CouponClaimDbModel.findOne({
where: { visitorId, couponCode },
});
if (usedBefore) {
res.status(403).send({ severity: 'error', message: COUPON_FRAUD_COPY.usedBefore });
return;
return NextResponse.json({ severity: 'error', message: COUPON_FRAUD_COPY.usedBefore }, { status: 403 });
}

// Check if visitor claimed another coupon recently
Expand All @@ -70,8 +58,12 @@ export default async function claimCouponHandler(req: NextApiRequest, res: NextA
},
});
if (usedAnotherCouponRecently) {
res.status(403).send({ severity: 'error', message: COUPON_FRAUD_COPY.usedAnotherCouponRecently });
return;
return NextResponse.json(
{ severity: 'error', message: COUPON_FRAUD_COPY.usedAnotherCouponRecently },
{
status: 403,
},
);
}

// If all checks passed, claim coupon
Expand All @@ -80,5 +72,5 @@ export default async function claimCouponHandler(req: NextApiRequest, res: NextA
visitorId,
timestamp: new Date(),
});
res.status(200).send({ severity: 'success', message: COUPON_FRAUD_COPY.success });
return NextResponse.json({ severity: 'success', message: COUPON_FRAUD_COPY.success });
}
9 changes: 9 additions & 0 deletions src/app/coupon-fraud/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 { CouponFraudUseCase } from '../CouponFraud';

export const metadata = generateUseCaseMetadata(USE_CASES.couponFraud);

export default function CouponFraudPage() {
return <CouponFraudUseCase />;
}
9 changes: 9 additions & 0 deletions src/app/coupon-fraud/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 { CouponFraudUseCase } from './CouponFraud';

export const metadata = generateUseCaseMetadata(USE_CASES.couponFraud);

export default function CoupounFraudPage() {
return <CouponFraudUseCase />;
}
File renamed without changes
File renamed without changes
12 changes: 1 addition & 11 deletions src/app/playground/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ import { ipBlocklistResult } from './components/IpBlocklistResult';
import { vpnDetectionResult } from './components/VpnDetectionResult';
import { usePlaygroundSignals } from './hooks/usePlaygroundSignals';
import { getLocationName, getZoomLevel } from '../../shared/utils/locationUtils';
import { FP_LOAD_OPTIONS } from '../../pages/_app';
import Link from 'next/link';
import styles from './playground.module.scss';
import { Spinner } from '../../client/components/common/Spinner/Spinner';
import { Alert } from '../../client/components/common/Alert/Alert';
import { timeAgoLabel } from '../../shared/timeUtils';
import { FpjsProvider } from '@fingerprintjs/fingerprintjs-pro-react';
import Container from '../../client/components/common/Container';
import { TEST_IDS } from '../../client/testIDs';
import tableStyles from './components/SignalTable.module.scss';
Expand Down Expand Up @@ -82,7 +80,7 @@ const TableTitle = ({ children }: { children: ReactNode }) => (
</motion.h3>
);

function Playground() {
export function Playground() {
const {
agentResponse,
isLoadingAgentResponse,
Expand Down Expand Up @@ -700,11 +698,3 @@ function Playground() {
</>
);
}

export default function PlaygroundPage() {
return (
<FpjsProvider loadOptions={FP_LOAD_OPTIONS}>
<Playground />
</FpjsProvider>
);
}
4 changes: 2 additions & 2 deletions src/app/playground/embed/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { PLAYGROUND_METADATA } from '../../../client/components/common/content';
import { generateUseCaseMetadata } from '../../../client/components/common/seo';
import PlaygroundPage from '../Playground';
import { Playground } from '../Playground';

export const metadata = generateUseCaseMetadata(PLAYGROUND_METADATA);

export default function VpnDetectionPage() {
return <PlaygroundPage />;
return <Playground />;
}
4 changes: 2 additions & 2 deletions src/app/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { PLAYGROUND_METADATA } from '../../client/components/common/content';
import { generateUseCaseMetadata } from '../../client/components/common/seo';
import PlaygroundPage from './Playground';
import { Playground } from './Playground';

export const metadata = generateUseCaseMetadata(PLAYGROUND_METADATA);

export default function VpnDetectionPage() {
return <PlaygroundPage />;
return <Playground />;
}
10 changes: 4 additions & 6 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '../styles/global-styles.scss';
import Head from 'next/head';
import { FpjsProvider, FingerprintJSPro } from '@fingerprintjs/fingerprintjs-pro-react';
import { FingerprintJSPro } from '@fingerprintjs/fingerprintjs-pro-react';
import { AppProps } from 'next/app';
import Providers from '../Providers';
import { Layout } from '../Layout';
Expand All @@ -24,11 +24,9 @@ function CustomApp({ Component, pageProps }: AppProps<CustomPageProps>) {
<title>Fingerprint Pro Use Cases</title>
</Head>
<Providers>
<FpjsProvider loadOptions={FP_LOAD_OPTIONS}>
<Layout embed={Boolean(pageProps.embed)}>
<Component {...pageProps} />
</Layout>
</FpjsProvider>
<Layout embed={Boolean(pageProps.embed)}>
<Component {...pageProps} />
</Layout>
</Providers>
</>
);
Expand Down
13 changes: 0 additions & 13 deletions src/pages/coupon-fraud/embed.tsx

This file was deleted.

8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5485,10 +5485,10 @@ sequelize-pool@^7.1.0:
resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz#210b391af4002762f823188fd6ecfc7413020768"
integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==

sequelize@^6.37.1:
version "6.37.1"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.37.1.tgz#9380fe0a3b5ff17638d3fce30c3cf3a2396c2343"
integrity sha512-vIKKzQ9dGp2aBOxQRD1FmUYViuQiKXSJ8yah8TsaBx4U3BokJt+Y2A0qz2C4pj08uX59qpWxRqSLEfRmVOEgQw==
sequelize@^6.37.3:
version "6.37.3"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.37.3.tgz#ed6212029a52c59a18638d2a703da84bc2f81311"
integrity sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==
dependencies:
"@types/debug" "^4.1.8"
"@types/validator" "^13.7.17"
Expand Down

0 comments on commit 2009679

Please sign in to comment.