From 0d219df2c0d269ea1b333e131b9d2d98f668a2fa Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Sun, 22 Sep 2024 12:58:54 +0100 Subject: [PATCH 1/4] chore: move web scraping page to `app` --- .../web-scraping/WebScraping.tsx} | 33 ++++++++++--------- src/app/web-scraping/embed/page.tsx | 9 +++++ src/app/web-scraping/page.tsx | 9 +++++ .../web-scraping/webScraping.module.scss | 0 src/pages/api/web-scraping/flights.ts | 2 +- src/pages/web-scraping/embed.tsx | 13 -------- 6 files changed, 36 insertions(+), 30 deletions(-) rename src/{pages/web-scraping/index.tsx => app/web-scraping/WebScraping.tsx} (89%) create mode 100644 src/app/web-scraping/embed/page.tsx create mode 100644 src/app/web-scraping/page.tsx rename src/{pages => app}/web-scraping/webScraping.module.scss (100%) delete mode 100644 src/pages/web-scraping/embed.tsx diff --git a/src/pages/web-scraping/index.tsx b/src/app/web-scraping/WebScraping.tsx similarity index 89% rename from src/pages/web-scraping/index.tsx rename to src/app/web-scraping/WebScraping.tsx index 2997fe3d..71a96f2e 100644 --- a/src/pages/web-scraping/index.tsx +++ b/src/app/web-scraping/WebScraping.tsx @@ -1,13 +1,13 @@ +'use client'; + import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper'; import FlightCard, { Flight } from '../../client/components/web-scraping/FlightCard'; import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react'; import { useQueryState } from 'next-usequerystate'; import { useQuery, UseQueryResult } from 'react-query'; -import { GetServerSideProps, NextPage } from 'next'; -import { FlightQuery } from '../api/web-scraping/flights'; +import { GetServerSideProps } from 'next'; import { CheckResultObject } from '../../server/checkResult'; import { USE_CASES } from '../../client/components/common/content'; -import { CustomPageProps } from '../_app'; import { Select, SelectItem } from '../../client/components/common/Select/Select'; import ArrowIcon from '../../client/img/arrowRight.svg'; import Image from 'next/image'; @@ -15,6 +15,9 @@ import styles from './webScraping.module.scss'; import Button from '../../client/components/common/Button/Button'; import { Alert } from '../../client/components/common/Alert/Alert'; import { Spinner } from '../../client/components/common/Spinner/Spinner'; +import { FlightQuery } from '../../pages/api/web-scraping/flights'; +import { FunctionComponent } from 'react'; +import { useSearchParams } from 'next/navigation'; // Make URL query object available as props to the page on first render // to read `from`, `to` params and a `disableBotDetection` param for testing and demo purposes @@ -66,14 +69,14 @@ type QueryAsProps = { disableBotDetection: boolean; }; -export const WebScrapingUseCase: NextPage = ({ - from, - to, - disableBotDetection, - embed, -}) => { - const [fromCode, setFromCode] = useQueryState('from', { defaultValue: from?.toUpperCase() ?? AIRPORTS[0].code }); - const [toCode, setToCode] = useQueryState('to', { defaultValue: to?.toUpperCase() ?? AIRPORTS[1].code }); +export const WebScrapingUseCase: FunctionComponent = () => { + const searchParams = useSearchParams(); + const [fromCode, setFromCode] = useQueryState('from', { + defaultValue: searchParams?.get('from')?.toUpperCase() ?? AIRPORTS[0].code, + }); + const [toCode, setToCode] = useQueryState('to', { + defaultValue: searchParams?.get('to')?.toUpperCase() ?? AIRPORTS[1].code, + }); /** * We use the Fingerprint Pro React SDK hook to get visitor data (https://github.com/fingerprintjs/fingerprintjs-pro-react) @@ -106,8 +109,8 @@ export const WebScrapingUseCase: NextPage = ({ from: fromCode, to: toCode, requestId, - disableBotDetection, - } as FlightQuery), + disableBotDetection: Boolean(searchParams?.get('disableBotDetection')), + } satisfies FlightQuery), }); if (response.status < 500) { return await response.json(); @@ -125,7 +128,7 @@ export const WebScrapingUseCase: NextPage = ({ return ( <> - +

Search for today's flights

{ @@ -203,5 +206,3 @@ const Results = ({ data, isFetching, error }: UseQueryResult ); }; - -export default WebScrapingUseCase; diff --git a/src/app/web-scraping/embed/page.tsx b/src/app/web-scraping/embed/page.tsx new file mode 100644 index 00000000..be0ceda1 --- /dev/null +++ b/src/app/web-scraping/embed/page.tsx @@ -0,0 +1,9 @@ +import { USE_CASES } from '../../../client/components/common/content'; +import { generateUseCaseMetadata } from '../../../client/components/common/seo'; +import { WebScrapingUseCase } from '../WebScraping'; + +export const metadata = generateUseCaseMetadata(USE_CASES.webScraping); + +export default function WebScrapingPage() { + return ; +} diff --git a/src/app/web-scraping/page.tsx b/src/app/web-scraping/page.tsx new file mode 100644 index 00000000..25fa1125 --- /dev/null +++ b/src/app/web-scraping/page.tsx @@ -0,0 +1,9 @@ +import { USE_CASES } from '../../client/components/common/content'; +import { generateUseCaseMetadata } from '../../client/components/common/seo'; +import { WebScrapingUseCase } from './WebScraping'; + +export const metadata = generateUseCaseMetadata(USE_CASES.webScraping); + +export default function WebScrapingPage() { + return ; +} diff --git a/src/pages/web-scraping/webScraping.module.scss b/src/app/web-scraping/webScraping.module.scss similarity index 100% rename from src/pages/web-scraping/webScraping.module.scss rename to src/app/web-scraping/webScraping.module.scss diff --git a/src/pages/api/web-scraping/flights.ts b/src/pages/api/web-scraping/flights.ts index f7b4236d..e334960a 100644 --- a/src/pages/api/web-scraping/flights.ts +++ b/src/pages/api/web-scraping/flights.ts @@ -2,9 +2,9 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { Severity, getAndValidateFingerprintResult } from '../../../server/checks'; import { isValidPostRequest } from '../../../server/server'; import { ONE_DAY_MS, FIVE_MINUTES_MS, ONE_HOUR_MS } from '../../../shared/timeUtils'; -import { AIRPORTS } from '../../web-scraping'; import { Flight } from '../../../client/components/web-scraping/FlightCard'; import { saveBotVisit } from '../../../server/botd-firewall/botVisitDatabase'; +import { AIRPORTS } from '../../../app/web-scraping/WebScraping'; const roundToFiveMinutes = (time: number) => Math.round(time / FIVE_MINUTES_MS) * FIVE_MINUTES_MS; diff --git a/src/pages/web-scraping/embed.tsx b/src/pages/web-scraping/embed.tsx deleted file mode 100644 index 313c4fbc..00000000 --- a/src/pages/web-scraping/embed.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { GetStaticProps } from 'next'; -import WebScraping from './index'; -import { CustomPageProps } from '../_app'; - -export default WebScraping; - -export const getStaticProps: GetStaticProps = async () => { - return { - props: { - embed: true, - }, - }; -}; From c22ec6747ce700ae4153786331a1476a4ae4f01b Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Sun, 22 Sep 2024 13:15:58 +0100 Subject: [PATCH 2/4] chore: move web scraping api to `app` --- src/app/web-scraping/WebScraping.tsx | 54 +----------------- .../web-scraping/api/flights/route.ts} | 56 +++++++++---------- src/app/web-scraping/data/airports.ts | 28 ++++++++++ src/server/checks.ts | 15 +++-- 4 files changed, 67 insertions(+), 86 deletions(-) rename src/{pages/api/web-scraping/flights.ts => app/web-scraping/api/flights/route.ts} (74%) create mode 100644 src/app/web-scraping/data/airports.ts diff --git a/src/app/web-scraping/WebScraping.tsx b/src/app/web-scraping/WebScraping.tsx index 71a96f2e..c0aebb68 100644 --- a/src/app/web-scraping/WebScraping.tsx +++ b/src/app/web-scraping/WebScraping.tsx @@ -5,7 +5,6 @@ import FlightCard, { Flight } from '../../client/components/web-scraping/FlightC import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react'; import { useQueryState } from 'next-usequerystate'; import { useQuery, UseQueryResult } from 'react-query'; -import { GetServerSideProps } from 'next'; import { CheckResultObject } from '../../server/checkResult'; import { USE_CASES } from '../../client/components/common/content'; import { Select, SelectItem } from '../../client/components/common/Select/Select'; @@ -15,60 +14,13 @@ import styles from './webScraping.module.scss'; import Button from '../../client/components/common/Button/Button'; import { Alert } from '../../client/components/common/Alert/Alert'; import { Spinner } from '../../client/components/common/Spinner/Spinner'; -import { FlightQuery } from '../../pages/api/web-scraping/flights'; +import { FlightQuery } from './api/flights/route'; import { FunctionComponent } from 'react'; import { useSearchParams } from 'next/navigation'; - -// Make URL query object available as props to the page on first render -// to read `from`, `to` params and a `disableBotDetection` param for testing and demo purposes -export const getServerSideProps: GetServerSideProps = async ({ query }) => { - const { from, to, disableBotDetection } = query; - return { - props: { - from: (from as string) ?? null, - to: (to as string) ?? null, - disableBotDetection: disableBotDetection === '1' || disableBotDetection === 'true', - }, - }; -}; +import { AIRPORTS } from './data/airports'; type FlightQueryResult = CheckResultObject; -export const AIRPORTS = [ - { city: 'San Francisco', code: 'SFO' }, - { city: 'New York', code: 'JFK' }, - { city: 'London', code: 'LHR' }, - { city: 'Tokyo', code: 'HND' }, - { city: 'Paris', code: 'CDG' }, - { city: 'Hong Kong', code: 'HKG' }, - { city: 'Singapore', code: 'SIN' }, - { city: 'Dubai', code: 'DXB' }, - { city: 'Shanghai', code: 'PVG' }, - { city: 'Seoul', code: 'ICN' }, - { city: 'Bangkok', code: 'BKK' }, - { city: 'Amsterdam', code: 'AMS' }, - { city: 'Beijing', code: 'PEK' }, - { city: 'Frankfurt', code: 'FRA' }, - { city: 'Cape Town', code: 'CPT' }, - { city: 'Sydney', code: 'SYD' }, - { city: 'Melbourne', code: 'MEL' }, - { city: 'Toronto', code: 'YYZ' }, - { city: 'Vancouver', code: 'YVR' }, - { city: 'Montreal', code: 'YUL' }, - { city: 'Brussels', code: 'BRU' }, - { city: 'Copenhagen', code: 'CPH' }, - { city: 'Oslo', code: 'OSL' }, - { city: 'Stockholm', code: 'ARN' }, - { city: 'Helsinki', code: 'HEL' }, - { city: 'Rome', code: 'FCO' }, -]; - -type QueryAsProps = { - from: string | null; - to: string | null; - disableBotDetection: boolean; -}; - export const WebScrapingUseCase: FunctionComponent = () => { const searchParams = useSearchParams(); const [fromCode, setFromCode] = useQueryState('from', { @@ -100,7 +52,7 @@ export const WebScrapingUseCase: FunctionComponent = () => { ['getFlights'], async () => { const { requestId } = await getVisitorData(); - const response = await fetch(`/api/web-scraping/flights`, { + const response = await fetch(`/web-scraping/api/flights`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/pages/api/web-scraping/flights.ts b/src/app/web-scraping/api/flights/route.ts similarity index 74% rename from src/pages/api/web-scraping/flights.ts rename to src/app/web-scraping/api/flights/route.ts index e334960a..feb294fc 100644 --- a/src/pages/api/web-scraping/flights.ts +++ b/src/app/web-scraping/api/flights/route.ts @@ -1,10 +1,9 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { Severity, getAndValidateFingerprintResult } from '../../../server/checks'; -import { isValidPostRequest } from '../../../server/server'; -import { ONE_DAY_MS, FIVE_MINUTES_MS, ONE_HOUR_MS } from '../../../shared/timeUtils'; -import { Flight } from '../../../client/components/web-scraping/FlightCard'; -import { saveBotVisit } from '../../../server/botd-firewall/botVisitDatabase'; -import { AIRPORTS } from '../../../app/web-scraping/WebScraping'; +import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks'; +import { ONE_DAY_MS, FIVE_MINUTES_MS, ONE_HOUR_MS } from '../../../../shared/timeUtils'; +import { Flight } from '../../../../client/components/web-scraping/FlightCard'; +import { saveBotVisit } from '../../../../server/botd-firewall/botVisitDatabase'; +import { NextRequest, NextResponse } from 'next/server'; +import { AIRPORTS } from '../../data/airports'; const roundToFiveMinutes = (time: number) => Math.round(time / FIVE_MINUTES_MS) * FIVE_MINUTES_MS; @@ -21,21 +20,17 @@ export type FlightsResponse = { data?: Flight[]; }; -export default async function getFlights(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 { from, to, requestId, disableBotDetection } = req.body as FlightQuery; +export async function POST(req: NextRequest): Promise> { + const { from, to, requestId, disableBotDetection } = (await req.json()) as FlightQuery; // Get the full Identification and Bot Detection result from Fingerprint Server API and validate its authenticity - const fingerprintResult = await getAndValidateFingerprintResult({ requestId, req }); + const fingerprintResult = await getAndValidateFingerprintResult({ + requestId, + req, + options: { minConfidenceScore: 0.5 }, + }); if (!fingerprintResult.okay) { - res.status(403).send({ severity: 'error', message: fingerprintResult.error }); - return; + return NextResponse.json({ severity: 'error', message: fingerprintResult.error }); } const identification = fingerprintResult.data.products?.identification?.data; @@ -44,35 +39,34 @@ export default async function getFlights(req: NextApiRequest, res: NextApiRespon // Backdoor for demo and testing purposes // If bot detection is disabled, just send the result if (!botData || disableBotDetection) { - res - .status(200) - .send({ severity: 'success', message: 'Bot detection is disabled.', data: getFlightResults(from, to) }); - return; + return NextResponse.json({ + severity: 'success', + message: 'Bot detection is disabled.', + data: getFlightResults(from, to), + }); } // If a bot is detected, return an error if (botData.bot?.result === 'bad') { - res.status(403).send({ - severity: 'error', - message: '🤖 Malicious bot detected, access denied.', - }); // 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; + return NextResponse.json({ + severity: 'error', + message: '🤖 Malicious bot detected, access denied.', + }); } // Check for unexpected bot detection value, just in case if (!['notDetected', 'good'].includes(botData.bot?.result)) { - res.status(500).send({ + return NextResponse.json({ severity: 'error', message: 'Server error, unexpected bot detection value.', }); - return; } // All checks passed, allow access - res.status(200).send({ + return NextResponse.json({ severity: 'success', message: 'No malicious bot nor spoofing detected, access allowed.', data: getFlightResults(from, to), diff --git a/src/app/web-scraping/data/airports.ts b/src/app/web-scraping/data/airports.ts new file mode 100644 index 00000000..61c00eb2 --- /dev/null +++ b/src/app/web-scraping/data/airports.ts @@ -0,0 +1,28 @@ +export const AIRPORTS = [ + { city: 'San Francisco', code: 'SFO' }, + { city: 'New York', code: 'JFK' }, + { city: 'London', code: 'LHR' }, + { city: 'Tokyo', code: 'HND' }, + { city: 'Paris', code: 'CDG' }, + { city: 'Hong Kong', code: 'HKG' }, + { city: 'Singapore', code: 'SIN' }, + { city: 'Dubai', code: 'DXB' }, + { city: 'Shanghai', code: 'PVG' }, + { city: 'Seoul', code: 'ICN' }, + { city: 'Bangkok', code: 'BKK' }, + { city: 'Amsterdam', code: 'AMS' }, + { city: 'Beijing', code: 'PEK' }, + { city: 'Frankfurt', code: 'FRA' }, + { city: 'Cape Town', code: 'CPT' }, + { city: 'Sydney', code: 'SYD' }, + { city: 'Melbourne', code: 'MEL' }, + { city: 'Toronto', code: 'YYZ' }, + { city: 'Vancouver', code: 'YVR' }, + { city: 'Montreal', code: 'YUL' }, + { city: 'Brussels', code: 'BRU' }, + { city: 'Copenhagen', code: 'CPH' }, + { city: 'Oslo', code: 'OSL' }, + { city: 'Stockholm', code: 'ARN' }, + { city: 'Helsinki', code: 'HEL' }, + { city: 'Rome', code: 'FCO' }, +]; diff --git a/src/server/checks.ts b/src/server/checks.ts index 8c63f773..285d366b 100644 --- a/src/server/checks.ts +++ b/src/server/checks.ts @@ -178,8 +178,9 @@ type GetFingerprintResultArgs = { serverApiKey?: string; region?: Region; options?: { - blockTor: boolean; - blockBots: boolean; + blockTor?: boolean; + blockBots?: boolean; + minConfidenceScore?: number; }; }; @@ -281,8 +282,14 @@ export const getAndValidateFingerprintResult = async ({ * This is context-sensitive and less reliable than the binary checks above, that's why it is checked last. * More info: https://dev.fingerprint.com/docs/understanding-your-confidence-score */ - if (identification?.confidence?.score && identification?.confidence?.score < env.MIN_CONFIDENCE_SCORE) { - return { okay: false, error: 'Identification confidence score too low, potential spoofing attack.' }; + if ( + identification?.confidence?.score && + identification?.confidence?.score < (options?.minConfidenceScore ?? env.MIN_CONFIDENCE_SCORE) + ) { + return { + okay: false, + error: `Identification confidence score too low (${identification?.confidence?.score}), potential spoofing attack.`, + }; } // All checks passed, we can trust this identification event From a479d5f0e50fb6ec13bd497d56906430365d43a5 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Sun, 22 Sep 2024 14:43:05 +0100 Subject: [PATCH 3/4] chore: move assets to app, fix build --- src/app/web-scraping/WebScraping.tsx | 15 ++++++++++++--- src/app/web-scraping/api/flights/route.ts | 2 +- .../components}/FlightCard.module.scss | 0 .../web-scraping/components}/FlightCard.tsx | 14 ++++++-------- .../img => app/web-scraping/images}/airCanada.svg | 0 .../img => app/web-scraping/images}/arrival.svg | 0 .../img => app/web-scraping/images}/departure.svg | 0 .../img => app/web-scraping/images}/star.svg | 0 8 files changed, 19 insertions(+), 12 deletions(-) rename src/{client/components/web-scraping => app/web-scraping/components}/FlightCard.module.scss (100%) rename src/{client/components/web-scraping => app/web-scraping/components}/FlightCard.tsx (94%) rename src/{client/img => app/web-scraping/images}/airCanada.svg (100%) rename src/{client/img => app/web-scraping/images}/arrival.svg (100%) rename src/{client/img => app/web-scraping/images}/departure.svg (100%) rename src/{client/img => app/web-scraping/images}/star.svg (100%) diff --git a/src/app/web-scraping/WebScraping.tsx b/src/app/web-scraping/WebScraping.tsx index c0aebb68..5b8557b5 100644 --- a/src/app/web-scraping/WebScraping.tsx +++ b/src/app/web-scraping/WebScraping.tsx @@ -1,7 +1,6 @@ 'use client'; import { UseCaseWrapper } from '../../client/components/common/UseCaseWrapper/UseCaseWrapper'; -import FlightCard, { Flight } from '../../client/components/web-scraping/FlightCard'; import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react'; import { useQueryState } from 'next-usequerystate'; import { useQuery, UseQueryResult } from 'react-query'; @@ -15,13 +14,14 @@ import Button from '../../client/components/common/Button/Button'; import { Alert } from '../../client/components/common/Alert/Alert'; import { Spinner } from '../../client/components/common/Spinner/Spinner'; import { FlightQuery } from './api/flights/route'; -import { FunctionComponent } from 'react'; +import { FunctionComponent, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import { AIRPORTS } from './data/airports'; +import { Flight, FlightCard } from './components/FlightCard'; type FlightQueryResult = CheckResultObject; -export const WebScrapingUseCase: FunctionComponent = () => { +const WebScraping: FunctionComponent = () => { const searchParams = useSearchParams(); const [fromCode, setFromCode] = useQueryState('from', { defaultValue: searchParams?.get('from')?.toUpperCase() ?? AIRPORTS[0].code, @@ -128,6 +128,15 @@ export const WebScrapingUseCase: FunctionComponent = () => { ); }; +export const WebScrapingUseCase = () => { + // Suspense required due to useSearchParams() https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout + return ( + + + + ); +}; + const Results = ({ data, isFetching, error }: UseQueryResult) => { const { data: flights, message, severity } = data ?? {}; diff --git a/src/app/web-scraping/api/flights/route.ts b/src/app/web-scraping/api/flights/route.ts index feb294fc..a990e9c4 100644 --- a/src/app/web-scraping/api/flights/route.ts +++ b/src/app/web-scraping/api/flights/route.ts @@ -1,9 +1,9 @@ import { Severity, getAndValidateFingerprintResult } from '../../../../server/checks'; import { ONE_DAY_MS, FIVE_MINUTES_MS, ONE_HOUR_MS } from '../../../../shared/timeUtils'; -import { Flight } from '../../../../client/components/web-scraping/FlightCard'; import { saveBotVisit } from '../../../../server/botd-firewall/botVisitDatabase'; import { NextRequest, NextResponse } from 'next/server'; import { AIRPORTS } from '../../data/airports'; +import { Flight } from '../../components/FlightCard'; const roundToFiveMinutes = (time: number) => Math.round(time / FIVE_MINUTES_MS) * FIVE_MINUTES_MS; diff --git a/src/client/components/web-scraping/FlightCard.module.scss b/src/app/web-scraping/components/FlightCard.module.scss similarity index 100% rename from src/client/components/web-scraping/FlightCard.module.scss rename to src/app/web-scraping/components/FlightCard.module.scss diff --git a/src/client/components/web-scraping/FlightCard.tsx b/src/app/web-scraping/components/FlightCard.tsx similarity index 94% rename from src/client/components/web-scraping/FlightCard.tsx rename to src/app/web-scraping/components/FlightCard.tsx index ba13b86a..d19cefe8 100644 --- a/src/client/components/web-scraping/FlightCard.tsx +++ b/src/app/web-scraping/components/FlightCard.tsx @@ -1,13 +1,13 @@ import { FunctionComponent } from 'react'; import styles from './FlightCard.module.scss'; -import DepartureIcon from '../../img/departure.svg'; -import ArrivalIcon from '../../img/arrival.svg'; -import AirCanada from '../../img/airCanada.svg'; +import DepartureIcon from '../images/departure.svg'; +import ArrivalIcon from '../images/arrival.svg'; +import AirCanada from '../images/airCanada.svg'; +import StarIcon from '../images/star.svg'; import Image from 'next/image'; -import Button from '../common/Button/Button'; -import StarIcon from '../../img/star.svg'; -import { TEST_IDS } from '../../testIDs'; import { ONE_HOUR_MS, ONE_MINUTE_MS } from '../../../shared/timeUtils'; +import { TEST_IDS } from '../../../client/testIDs'; +import Button from '../../../client/components/common/Button/Button'; const TEST_ID = TEST_IDS.webScraping; @@ -174,5 +174,3 @@ export const FlightCard: FunctionComponent = ({ flight }) => { ); }; - -export default FlightCard; diff --git a/src/client/img/airCanada.svg b/src/app/web-scraping/images/airCanada.svg similarity index 100% rename from src/client/img/airCanada.svg rename to src/app/web-scraping/images/airCanada.svg diff --git a/src/client/img/arrival.svg b/src/app/web-scraping/images/arrival.svg similarity index 100% rename from src/client/img/arrival.svg rename to src/app/web-scraping/images/arrival.svg diff --git a/src/client/img/departure.svg b/src/app/web-scraping/images/departure.svg similarity index 100% rename from src/client/img/departure.svg rename to src/app/web-scraping/images/departure.svg diff --git a/src/client/img/star.svg b/src/app/web-scraping/images/star.svg similarity index 100% rename from src/client/img/star.svg rename to src/app/web-scraping/images/star.svg From 02550c1eaf52c1e211bcb252c6c128d89e00e803 Mon Sep 17 00:00:00 2001 From: Juraj Uhlar Date: Sun, 22 Sep 2024 14:46:50 +0100 Subject: [PATCH 4/4] chore: add status codes --- src/app/web-scraping/api/flights/route.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/web-scraping/api/flights/route.ts b/src/app/web-scraping/api/flights/route.ts index a990e9c4..5d5d3431 100644 --- a/src/app/web-scraping/api/flights/route.ts +++ b/src/app/web-scraping/api/flights/route.ts @@ -30,7 +30,7 @@ export async function POST(req: NextRequest): Promise