Skip to content

Commit bcdf32c

Browse files
committed
validate monthName route param, add 404 and 500 pages
1 parent bb1807a commit bcdf32c

File tree

13 files changed

+136
-30
lines changed

13 files changed

+136
-30
lines changed

.github/workflows/build-push-docker.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Build and push Docker
33
on:
44
push:
55
branches:
6-
- 'disabled-main'
6+
- 'main'
77
tags:
88
- 'v[0-9]+.[0-9]+.[0-9]+'
99
pull_request:

.github/workflows/deploy-docker.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Deploy Docker
33
on:
44
workflow_run:
55
workflows:
6-
- 'disabled-Build and push Docker'
6+
- 'Build and push Docker'
77
types:
88
- completed
99

app/[[...month]]/page.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FC } from 'react';
2+
import { notFound } from 'next/navigation';
23

34
import LineChartMultiple from '@/components/charts/line-chart-multiple';
45
import Heading from '@/components/heading';
@@ -8,6 +9,7 @@ import { getNewOldCompaniesForMonthCached } from '@/modules/database/select/comp
89
import { getNewOldCompaniesCountForAllMonthsCached } from '@/modules/database/select/line-chart';
910
import { getAllMonths } from '@/modules/database/select/month';
1011
import { getStatisticsCached } from '@/modules/database/select/statistics';
12+
import { isValidMonthNameWithDb } from '@/utils/validation';
1113
import { METADATA } from '@/constants/metadata';
1214

1315
import { MonthQueryParam } from '@/types/website';
@@ -26,6 +28,8 @@ const IndexPage: FC<Props> = async ({ params }) => {
2628
const { month } = await params;
2729
const selectedMonth = month?.[0] ?? allMonths[0].name;
2830

31+
if (!isValidMonthNameWithDb(selectedMonth)) return notFound();
32+
2933
const newOldCompanies = await getNewOldCompaniesForMonthCached(selectedMonth);
3034

3135
const { monthsCount, companiesCount, commentsCount, firstMonth, lastMonth } = statistics ?? {

app/global-error.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use client';
2+
3+
import { FC } from 'react';
4+
import Link from 'next/link';
5+
6+
import { RefreshCcw, Settings } from 'lucide-react';
7+
8+
import { Button } from '@/components/ui/button';
9+
10+
import { fontSans } from '@/libs/fonts';
11+
import { cn } from '@/utils/styles';
12+
13+
import '@/styles/globals.css';
14+
15+
interface Props {
16+
error: Error & { digest?: string };
17+
reset: () => void;
18+
}
19+
20+
const GlobalError: FC<Props> = ({ error, reset }) => (
21+
<html>
22+
<body
23+
className={cn(
24+
'relative min-h-screen min-w-80 flex flex-col bg-background font-sans antialiased',
25+
fontSans.variable
26+
)}
27+
>
28+
<main className="flex-1 flex flex-col justify-center items-center p-4">
29+
<div className="w-full max-w-md space-y-4 text-center">
30+
<div className="flex justify-center">
31+
<Settings className="size-24 text-muted-foreground" />
32+
</div>
33+
34+
<h1 className="text-3xl font-extrabold tracking-tight">500 - Server Error</h1>
35+
<p className="text-muted-foreground">
36+
We are sorry, but something went wrong on our end. Please try again later.
37+
</p>
38+
39+
<div className="flex flex-col sm:flex-row gap-4 justify-center">
40+
<Button onClick={() => reset()} variant="outline">
41+
<RefreshCcw className="mr-2 size-4" /> Try Again
42+
</Button>
43+
<Button asChild>
44+
<Link href="/">Return to Home</Link>
45+
</Button>
46+
</div>
47+
48+
{error.digest && (
49+
<p className="text-sm text-muted-foreground">Error ID: {error.digest}</p>
50+
)}
51+
</div>
52+
</main>
53+
</body>
54+
</html>
55+
);
56+
57+
export default GlobalError;

app/layout.tsx

+22-20
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { SERVER_CONFIG } from '@/config/server';
1313

1414
import '@/styles/globals.css';
1515

16+
import { FC } from 'react';
17+
1618
// single in layout is enough for all pages
1719
export const dynamic = 'force-dynamic';
1820

@@ -69,24 +71,24 @@ interface RootLayoutProps {
6971
children: React.ReactNode;
7072
}
7173

72-
export default function RootLayout({ children }: RootLayoutProps) {
73-
return (
74-
<html lang="en" suppressHydrationWarning>
75-
<BaseHead />
76-
<body
77-
className={cn(
78-
'relative min-h-screen min-w-80 flex flex-col bg-background font-sans antialiased',
79-
fontSans.variable
80-
)}
81-
>
82-
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
83-
<Header />
84-
<main className="flex-1 my-container py-8 md:py-10">{children}</main>
85-
<Footer />
74+
const RootLayout: FC<RootLayoutProps> = async ({ children }) => (
75+
<html lang="en" suppressHydrationWarning>
76+
<BaseHead />
77+
<body
78+
className={cn(
79+
'relative min-h-screen min-w-80 flex flex-col bg-background font-sans antialiased',
80+
fontSans.variable
81+
)}
82+
>
83+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
84+
<Header />
85+
<main className="flex-1 flex flex-col my-container py-8 md:py-10">{children}</main>
86+
<Footer />
8687

87-
<TailwindIndicator />
88-
</ThemeProvider>
89-
</body>
90-
</html>
91-
);
92-
}
88+
<TailwindIndicator />
89+
</ThemeProvider>
90+
</body>
91+
</html>
92+
);
93+
94+
export default RootLayout;

app/month/[[...month]]/page.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FC } from 'react';
2+
import { notFound } from 'next/navigation';
23

34
import BarChartSimple from '@/components/charts/bar-chart-simple';
45
import CompaniesCommentsTable from '@/components/companies-comments-table';
@@ -8,6 +9,7 @@ import { getNewOldCompaniesForMonthCached } from '@/modules/database/select/comp
89
import { getAllMonths } from '@/modules/database/select/month';
910
import { getBarChartSimpleData } from '@/modules/transform/bar-chart';
1011
import { getCompanyTableData } from '@/modules/transform/table';
12+
import { isValidMonthNameWithDb } from '@/utils/validation';
1113

1214
import { MonthQueryParam } from '@/types/api';
1315

@@ -19,6 +21,8 @@ const CurrentMonthPage: FC<Props> = async ({ params }) => {
1921
const { month } = await params;
2022
const selectedMonth = month?.[0] ?? allMonths[0].name;
2123

24+
if (!isValidMonthNameWithDb(selectedMonth)) return notFound();
25+
2226
const newOldCompanies = await getNewOldCompaniesForMonthCached(selectedMonth);
2327
const companyTableData = getCompanyTableData(newOldCompanies.allCompanies);
2428
const barChartSimpleData = getBarChartSimpleData(newOldCompanies.allCompanies);

app/not-found.tsx

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { FC } from 'react';
2+
import Link from 'next/link';
3+
4+
import { Cat } from 'lucide-react';
5+
6+
import { Button } from '@/components/ui/button';
7+
8+
const NotFound: FC = () => (
9+
<div className="flex-1 flex flex-col justify-center items-center">
10+
<div className="space-y-4 text-center">
11+
<div className="flex justify-center">
12+
<Cat className="size-24 text-muted-foreground" />
13+
</div>
14+
15+
<h1 className="text-3xl font-extrabold tracking-tight">404 - Page Not Found</h1>
16+
<p className="text-muted-foreground">Oops! The page you are looking for does not exist.</p>
17+
18+
<div className="flex flex-col sm:flex-row gap-4 justify-center">
19+
<Button asChild variant="outline">
20+
<Link href="/">Return to Home</Link>
21+
</Button>
22+
</div>
23+
</div>
24+
</div>
25+
);
26+
27+
export default NotFound;

constants/validation.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const VALIDATION = {
2+
monthNameRegex: /^\d{4}-\d{2}$/,
3+
} as const;

docs/working-notes/notes4.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
record video demo
33
database diagram
44
fix bar chart 8-12
5-
fix first month exception
6-
add 404 and 500 page
5+
fix first month exception
6+
add 404 and 500 page
7+
validate month in page params

libs/datetime.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ const { appTimeZone, appDateTimeFormat } = SERVER_CONFIG;
1616
export const DATETIME = {
1717
monthNameFormat: 'yyyy-MM',
1818
sanFranciscoTimeZone: 'America/Los_Angeles',
19-
monthNameRegex: /^\d{4}-\d{2}$/,
2019
} as const;
2120

22-
const { monthNameFormat, sanFranciscoTimeZone, monthNameRegex } = DATETIME;
21+
const { monthNameFormat, sanFranciscoTimeZone } = DATETIME;
2322

2423
/**
2524
* Format to 'YYYY-MM'.
@@ -88,6 +87,3 @@ export const createOldMonthName = (monthName: string, monthsAgo: number): string
8887
const previousDate = subMonths(date, monthsAgo);
8988
return format(previousDate, monthNameFormat);
9089
};
91-
92-
/** Used only in db to validate for delete. */
93-
export const isValidMonthName = (monthName: string): boolean => monthNameRegex.test(monthName);

modules/database/delete.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getDb } from '@/modules/database/schema';
2-
import { isValidMonthName } from '@/libs/datetime';
2+
import { isValidMonthName } from '@/utils/validation';
33
import { ALGOLIA } from '@/constants/algolia';
44

55
const { threads } = ALGOLIA;

modules/database/select/month.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getDb } from '@/modules/database/schema';
22

33
import { DbMonth, MonthPair } from '@/types/database';
44

5+
// never cache this, validate month slug
56
export const getMonthByName = (monthName: string): DbMonth => {
67
const month = getDb()
78
.prepare<string, DbMonth>(`SELECT * FROM month WHERE name = ?`)

utils/validation.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { getMonthByName } from '@/modules/database/select/month';
2+
import { VALIDATION } from '@/constants/validation';
3+
4+
const { monthNameRegex } = VALIDATION;
5+
6+
/** Used only in db to validate for delete. */
7+
export const isValidMonthName = (monthName: string): boolean => monthNameRegex.test(monthName);
8+
9+
/** In pages, validate month slug. */
10+
export const isValidMonthNameWithDb = (monthName: string): boolean =>
11+
isValidMonthName(monthName) && Boolean(getMonthByName(monthName));

0 commit comments

Comments
 (0)