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 ( - - setIsOpen(false)} className="fixed z-40 inset-0 flex items-center justify-center"> - -
- + setIsOpen(false)} + > + - - - {children} - - - - + + {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" } }