관리자 권한 거부 화면의 HTTP 경계를 403으로 변경

이 글의 목차 9개 항목
  • 분야: BE
  • 기술 스택: TypeScript · Next.js API · vinext · Cloudflare Workers
  • 관련 기술: 서버 페이지 세션 · forbidden() · SSR 상태 코드 · 계정 전환

문제와 원인

이전 관리자 세션 함수는 학습자에게 status: "forbidden" 객체를 반환했다. 페이지는 이 값을 받아 일반 HTML에 “403”과 접근 거부 문구를 출력해, 권한 거부를 서버 응답 상태로 전달하는 경계가 필요했다.

수정 전 코드

loadAdminPageSession()

if (!isAdminEmail(user.email)) {
  return {
    status: "forbidden",
    signOutPath: applicationSignOutPath("/admin"),
  };
}

AdminPage()

if (session.status === "forbidden") {
  return (
    <main className="admin-access-page">
      <section className="admin-access-card">
        <span>403</span>
        <h1>관리자 권한이 없습니다.</h1>
        {/* 안내와 계정 전환 링크 */}
      </section>
    </main>
  );
}

수정 후 코드

학습자는 forbidden()으로 종료하고 반환 타입을 authorized 세션으로 좁혔다. 관리자 페이지의 거부 상태 렌더 분기와 SKCT 개인 관리 페이지의 별도 검사도 제거했다.

loadAdminPageSession()

export async function loadAdminPageSession(
  returnTo: string,
): Promise<Extract<AdminPageSession, { status: "authorized" }>> {
  const user = await requireSiteUser(returnTo);
  const { isAdminEmail } = await import("@backend/common/auth/admin-auth");
  if (!isAdminEmail(user.email)) forbidden();

  return {
    status: "authorized",
    displayName: user.displayName,
    signOutPath: applicationSignOutPath("/"),
  };
}

AdminPage()

export default async function AdminPage({
  initialSection,
  session,
}: {
  initialSection: AdminSection;
  session: Extract<AdminPageSession, { status: "authorized" }>;
}) {
  return (
    <AdminApp
      key={initialSection}
      initialSection={initialSection}
      displayName={session.displayName}
      signOutPath={session.signOutPath}
    />
  );
}

Forbidden()

거부 화면은 관리자 경로의 forbidden.tsx로 옮겼다. 권한 안내와 다른 계정으로 로그인하는 POST 폼을 유지하며, vinext의 NEXT_HTTP_ERROR_FALLBACK;403 오류가 서버 응답 경계를 지정한다.

export default function Forbidden() {
  return (
    <main className="admin-access-page">
      <section className="admin-access-card">
        <span>403</span>
        <h1>관리자 권한이 없습니다.</h1>
        <p>이 페이지는 모두의 문제집 운영자 계정으로만 접근할 수 있습니다.</p>
        <div>
          <Link href="/">학습 사이트로 돌아가기</Link>
          <SignOutForm action={applicationSignOutPath("/admin")}>
            다른 계정으로 로그인
          </SignOutForm>
        </div>
      </section>
    </main>
  );
}

검증 결과

Node.js에서 수정 전후 원본 loadAdminPageSession()에 합성 사용자 정보를 연결했다. HTTP 검증은 현재 빌드 Worker·SQLite·합성 서명 세션 조건에서 경로별 1회 실행했다.

사용자 역할수정 전 세션 함수수정 후 세션 함수
학습자forbidden 객체 반환반환 전에 HTTP 403 접근 오류
관리자authorized 반환authorized 반환
현재 로컬 Worker 요청HTTP응답 내용
학습자 /admin403거부 안내·계정 전환 POST 폼
학습자 /admin/members403거부 안내·계정 전환 POST 폼
학습자 /admin/skct-personal403거부 안내·계정 전환 POST 폼
관리자 /admin200정상 관리자 응답

학습자 3/3건은 HTTP 403, 관리자 1/1건은 HTTP 200을 반환했다. 같은 Worker의 /admin을 메모리 SQLite·합성 학습자 세션으로 1회 요청한 Swagger UI 화면(1,280×720 CSS px·DPR 2)에서도 HTTP 403 응답이 표시됐다.

학습자 관리자 페이지 요청의 HTTP 403 응답원본 보기 (새 창)

학습자 관리자 페이지 요청의 HTTP 403 응답

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.