From 0e3576c8d351541029e0d935f293a5082befe190 Mon Sep 17 00:00:00 2001 From: saeyeon-05 Date: Wed, 7 Oct 2026 19:05:55 +0900 Subject: [PATCH] fix(fe): restrict contest pages to contest admin and manager --- .../_components/ContestAccessDenied.tsx | 23 +++++++++++ .../app/admin/contest/[contestId]/layout.tsx | 41 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 apps/frontend/app/admin/contest/[contestId]/_components/ContestAccessDenied.tsx create mode 100644 apps/frontend/app/admin/contest/[contestId]/layout.tsx diff --git a/apps/frontend/app/admin/contest/[contestId]/_components/ContestAccessDenied.tsx b/apps/frontend/app/admin/contest/[contestId]/_components/ContestAccessDenied.tsx new file mode 100644 index 0000000000..3dfbd5d32d --- /dev/null +++ b/apps/frontend/app/admin/contest/[contestId]/_components/ContestAccessDenied.tsx @@ -0,0 +1,23 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { useEffect, useRef } from 'react' +import { toast } from 'sonner' + +export function ContestAccessDenied() { + const router = useRouter() + const done = useRef(false) + + useEffect(() => { + if (done.current) { + return + } + done.current = true + router.replace('/admin/contest') + setTimeout(() => { + toast.error('You do not have access to this contest.') + }, 100) + }, [router]) + + return null +} diff --git a/apps/frontend/app/admin/contest/[contestId]/layout.tsx b/apps/frontend/app/admin/contest/[contestId]/layout.tsx new file mode 100644 index 0000000000..5cec43a361 --- /dev/null +++ b/apps/frontend/app/admin/contest/[contestId]/layout.tsx @@ -0,0 +1,41 @@ +import { safeFetcherWithAuth } from '@/libs/utils' +import { ContestRole, type UserContest } from '@generated/graphql' +import { ContestAccessDenied } from './_components/ContestAccessDenied' + +export const dynamic = 'force-dynamic' + +async function fetchContestRoles() { + try { + const response: UserContest[] = await safeFetcherWithAuth + .get('contest/role') + .json() + + return response + } catch (error) { + console.error('Error fetching contest roles:', error) + return [] + } +} + +export default async function Layout({ + children, + params +}: { + children: React.ReactNode + params: Promise<{ contestId: string }> +}) { + const { contestId } = await params + const roles = await fetchContestRoles() + + const hasPermission = roles.some( + (r) => + r.contestId === Number(contestId) && + (r.role === ContestRole.Admin || r.role === ContestRole.Manager) + ) + + if (!hasPermission) { + return + } + + return children +}