diff --git a/app/login-handler/route.ts b/app/login-handler/route.ts
new file mode 100644
index 0000000..23d954d
--- /dev/null
+++ b/app/login-handler/route.ts
@@ -0,0 +1,17 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { login } from '@/util/auth';
+
+
+export async function GET(req: NextRequest) {
+ const params = req.nextUrl.searchParams;
+
+ const token = params.get('token');
+ if (!token)
+ return NextResponse.redirect(new URL('/login', req.url));
+
+ const res = await login(token);
+ if ('error' in res)
+ return NextResponse.redirect(new URL(`/login?error=${res.error}`, req.url));
+
+ return NextResponse.redirect(new URL('/profile', req.url));
+}
diff --git a/app/login/LoginContent.tsx b/app/login/LoginContent.tsx
index d083026..422773a 100644
--- a/app/login/LoginContent.tsx
+++ b/app/login/LoginContent.tsx
@@ -1,7 +1,7 @@
'use client'
-import { useLayoutEffect, useState } from 'react';
-import { useRouter, useSearchParams } from 'next/navigation';
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { login } from '@/util/auth';
@@ -12,20 +12,14 @@ import IconInput from '@/components/IconInput';
import { FaAddressCard } from 'react-icons/fa6';
-export default function LoginContent() {
+type LoginContentProps = {
+ error?: string
+}
+export default function LoginContent(props: LoginContentProps) {
const [teamToken, setTeamToken] = useState('');
- const [error, setError] = useState('');
+ const [error, setError] = useState(props.error ?? '');
const router = useRouter();
- const params = useSearchParams();
-
- // Automatically sign in if the `token` URL search parameter is set.
- useLayoutEffect(() => {
- const token = params.get('token');
- if (!token) return;
-
- void loginCallback(token);
- }, [params.get('token')]);
async function loginCallback(teamToken: string) {
const res = await login(teamToken);
diff --git a/app/login/page.tsx b/app/login/page.tsx
index 6ee4249..2617308 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -1,4 +1,7 @@
import type { Metadata } from 'next';
+import { redirect } from 'next/navigation';
+
+// Components
import LoginContent from '@/app/login/LoginContent';
@@ -6,14 +9,20 @@ export const metadata: Metadata = {
title: 'Log in'
}
-export default function Login() {
+export default async function Login({ searchParams }: { searchParams: Promise<{ token?: string, error?: string }> }) {
+ const token = (await searchParams).token;
+ const error = (await searchParams).error;
+
+ // Automatically sign in if the `token` search parameter is set.
+ if (token) return redirect(`/login-handler?token=${token}`)
+
return (
Log in to b01lers internal CTF
-
+
)
}
diff --git a/app/profile/[id]/page.tsx b/app/profile/[id]/page.tsx
index c6b2aad..423c684 100644
--- a/app/profile/[id]/page.tsx
+++ b/app/profile/[id]/page.tsx
@@ -8,8 +8,8 @@ import Profile from '@/app/profile/Profile';
import { getProfile } from '@/util/profile';
-export async function generateMetadata({ params }: { params: { id: string } }): Promise {
- const data = await getProfile(params.id);
+export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise {
+ const data = await getProfile((await params).id);
if (data.kind === 'badUnknownUser') return notFound();
return {
@@ -17,8 +17,8 @@ export async function generateMetadata({ params }: { params: { id: string } }):
}
}
-export default async function ProfilePage({ params }: { params: { id: string } }) {
- const data = await getProfile(params.id);
+export default async function ProfilePage({ params }: { params: Promise<{ id: string }> }) {
+ const data = await getProfile((await params).id);
if (data.kind === 'badUnknownUser') return notFound();
return (
diff --git a/app/verify/page.tsx b/app/verify/page.tsx
index b61bb68..dee2662 100644
--- a/app/verify/page.tsx
+++ b/app/verify/page.tsx
@@ -13,11 +13,12 @@ export const metadata: Metadata = {
title: 'Verify'
}
-export default async function Verify({ searchParams }: { searchParams: { token: string } }) {
- if (!searchParams.token)
+export default async function Verify({ searchParams }: { searchParams: Promise<{ token: string }> }) {
+ const token = (await searchParams).token;
+ if (!token)
return redirect('/register');
- const res = await verify(searchParams.token);
+ const res = await verify(token);
if (res.kind === 'goodEmailSet')
return redirect('/profile');
if (res.kind !== 'goodRegister' && res.kind !== 'goodVerify')
diff --git a/components/CenteredModal.tsx b/components/CenteredModal.tsx
index 6cbd2a0..30078e6 100644
--- a/components/CenteredModal.tsx
+++ b/components/CenteredModal.tsx
@@ -1,44 +1,34 @@
-import { Fragment, ReactNode } from 'react';
-import { Dialog, Transition } from '@headlessui/react';
+import type { ReactNode } from 'react';
+import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react';
// A reusable component to wrap a transition and dialog overlay around a screen-centered div.
type CenteredModalProps = {
- isOpen: boolean, setIsOpen: (isOpen: boolean) => void,
- className: string, children: ReactNode
+ isOpen: boolean,
+ setIsOpen: (isOpen: boolean) => void,
+ className: string,
+ children: ReactNode
}
export default function CenteredModal(props: CenteredModalProps) {
const { isOpen, setIsOpen, className, children } = props;
return (
-
-
+
+ {children}
+
+
)
}
diff --git a/package-lock.json b/package-lock.json
index 8f6e2dc..f7d70a4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"name": "bctf",
"version": "0.1.0",
"dependencies": {
- "@headlessui/react": "^1.7.18",
+ "@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.0",
"autoprefixer": "^10.4.20",
"luxon": "^3.5.0",
@@ -18,7 +18,7 @@
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
- "recharts": "^2.12.2",
+ "recharts": "^2.13.3",
"tailwindcss": "^3.4.10",
"typescript": "5.3.3"
},
@@ -60,20 +60,70 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+ "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
+ "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.8"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.26.28",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
+ "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.2",
+ "@floating-ui/utils": "^0.2.8",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
+ },
"node_modules/@headlessui/react": {
- "version": "1.7.18",
- "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
- "integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
+ "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
"dependencies": {
- "@tanstack/react-virtual": "^3.0.0-beta.60",
- "client-only": "^0.0.1"
+ "@floating-ui/react": "^0.26.16",
+ "@react-aria/focus": "^3.17.1",
+ "@react-aria/interactions": "^3.21.3",
+ "@tanstack/react-virtual": "^3.8.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
- "react": "^16 || ^17 || ^18",
- "react-dom": "^16 || ^17 || ^18"
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@headlessui/tailwindcss": {
@@ -654,6 +704,83 @@
"node": ">=14"
}
},
+ "node_modules/@react-aria/focus": {
+ "version": "3.18.4",
+ "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.18.4.tgz",
+ "integrity": "sha512-91J35077w9UNaMK1cpMUEFRkNNz0uZjnSwiyBCFuRdaVuivO53wNC9XtWSDNDdcO5cGy87vfJRVAiyoCn/mjqA==",
+ "dependencies": {
+ "@react-aria/interactions": "^3.22.4",
+ "@react-aria/utils": "^3.25.3",
+ "@react-types/shared": "^3.25.0",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/interactions": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.22.4.tgz",
+ "integrity": "sha512-E0vsgtpItmknq/MJELqYJwib+YN18Qag8nroqwjk1qOnBa9ROIkUhWJerLi1qs5diXq9LHKehZDXRlwPvdEFww==",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.6",
+ "@react-aria/utils": "^3.25.3",
+ "@react-types/shared": "^3.25.0",
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/ssr": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.6.tgz",
+ "integrity": "sha512-iLo82l82ilMiVGy342SELjshuWottlb5+VefO3jOQqQRNYnJBFpUSadswDPbRimSgJUZuFwIEYs6AabkP038fA==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-aria/utils": {
+ "version": "3.25.3",
+ "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.3.tgz",
+ "integrity": "sha512-PR5H/2vaD8fSq0H/UB9inNbc8KDcVmW6fYAfSWkkn+OAdhTTMVKqXXrZuZBWyFfSD5Ze7VN6acr4hrOQm2bmrA==",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.6",
+ "@react-stately/utils": "^3.10.4",
+ "@react-types/shared": "^3.25.0",
+ "@swc/helpers": "^0.5.0",
+ "clsx": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-stately/utils": {
+ "version": "3.10.4",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.4.tgz",
+ "integrity": "sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@react-types/shared": {
+ "version": "3.25.0",
+ "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.25.0.tgz",
+ "integrity": "sha512-OZSyhzU6vTdW3eV/mz5i6hQwQUhkRs7xwY2d1aqPvTdMe0+2cY7Fwp45PAiwYLEj73i9ro2FxF9qC4DvHGSCgQ==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -668,11 +795,11 @@
}
},
"node_modules/@tanstack/react-virtual": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.1.3.tgz",
- "integrity": "sha512-YCzcbF/Ws/uZ0q3Z6fagH+JVhx4JLvbSflgldMgLsuvB8aXjZLLb3HvrEVxY480F9wFlBiXlvQxOyXb5ENPrNA==",
+ "version": "3.10.9",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.9.tgz",
+ "integrity": "sha512-OXO2uBjFqA4Ibr2O3y0YMnkrRWGVNqcvHQXmGvMu6IK8chZl3PrDxFXdGZ2iZkSrKh3/qUYoFqYe+Rx23RoU0g==",
"dependencies": {
- "@tanstack/virtual-core": "3.1.3"
+ "@tanstack/virtual-core": "3.10.9"
},
"funding": {
"type": "github",
@@ -684,9 +811,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.1.3.tgz",
- "integrity": "sha512-Y5B4EYyv1j9V8LzeAoOVeTg0LI7Fo5InYKgAjkY1Pu9GjtUwX/EKxNcU7ng3sKr99WEf+bPTcktAeybyMOYo+g==",
+ "version": "3.10.9",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz",
+ "integrity": "sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
@@ -2895,9 +3022,9 @@
}
},
"node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ "version": "19.0.0-rc-fb9a90fa48-20240614",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0-rc-fb9a90fa48-20240614.tgz",
+ "integrity": "sha512-60qI7v1B9RhmZwjTCnAgzcuABOQsIH20vTbETQPaze96s1lY2lSawv9dvXAfF8Z1MIqOppWSKLNOshF0WsZ3OA=="
},
"node_modules/react-markdown": {
"version": "9.0.1",
@@ -2973,14 +3100,14 @@
}
},
"node_modules/recharts": {
- "version": "2.12.2",
- "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.2.tgz",
- "integrity": "sha512-9bpxjXSF5g81YsKkTSlaX7mM4b6oYI1mIYck6YkUcWuL3tomADccI51/6thY4LmvhYuRTwpfrOvE80Zc3oBRfQ==",
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.3.tgz",
+ "integrity": "sha512-YDZ9dOfK9t3ycwxgKbrnDlRC4BHdjlY73fet3a0C1+qGMjXVZe6+VXmpOIIhzkje5MMEL8AN4hLIe4AMskBzlA==",
"dependencies": {
"clsx": "^2.0.0",
"eventemitter3": "^4.0.1",
"lodash": "^4.17.21",
- "react-is": "^16.10.2",
+ "react-is": "^18.3.1",
"react-smooth": "^4.0.0",
"recharts-scale": "^0.4.4",
"tiny-invariant": "^1.3.1",
@@ -3371,6 +3498,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
+ },
"node_modules/tailwindcss": {
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
diff --git a/package.json b/package.json
index 7b64419..b9a84ed 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
- "@headlessui/react": "^1.7.18",
+ "@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.0",
"autoprefixer": "^10.4.20",
"luxon": "^3.5.0",
@@ -19,7 +19,7 @@
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-markdown": "^9.0.1",
- "recharts": "^2.12.2",
+ "recharts": "^2.13.3",
"tailwindcss": "^3.4.10",
"typescript": "5.3.3"
},
@@ -28,5 +28,8 @@
"@types/node": "^22.5.0",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0"
+ },
+ "overrides": {
+ "react-is": "^19.0.0-beta-26f2496093-20240514"
}
}