概要
ユーザーのロール(admin / editor / viewer)に基づいて UI 要素の表示・非表示を切り替え、権限のないページには forbidden() でアクセスを遮断する。Server Component でのガードと Client Component での表示制御の両方を実装し、forbidden() との責務の違いも示す。
インストール
# 追加インストールは不要
実装
ロール型とガードユーティリティ
// lib/auth.ts(抜粋)
export type UserRole = "admin" | "editor" | "viewer";
export type AuthUser = {
id: string;
name: string;
role: UserRole;
};
/** 指定ロール以上の権限があるか確認する */
export function hasRole(user: AuthUser | null, required: UserRole): boolean {
if (!user) return false;
const hierarchy: UserRole[] = ["viewer", "editor", "admin"];
return hierarchy.indexOf(user.role) >= hierarchy.indexOf(required);
}
Server Component でのページガード
// app/admin/page.tsx
import { forbidden } from "next/navigation";
import { getCurrentUser, hasRole } from "@/lib/auth";
export default async function AdminPage() {
const user = await getCurrentUser();
if (!user) {
// 未ログイン → forbidden または redirect
forbidden();
}
if (!hasRole(user, "admin")) {
// ログイン済みだが admin 未満 → 403
forbidden();
}
return (
<div>
<h1>管理者ページ</h1>
{/* ...管理コンテンツ */}
</div>
);
}
Server Component での UI 要素制御
// app/samples/[slug]/page.tsx(抜粋)
import { getCurrentUser, hasRole } from "@/lib/auth";
export default async function SampleDetailPage({ params }: Props) {
const { slug } = await params;
const user = await getCurrentUser();
return (
<div>
<h1>{slug}</h1>
{/* editor 以上のみ編集ボタンを表示 */}
{hasRole(user, "editor") && (
<a
href={`/admin/samples/${slug}/edit`}
className="rounded bg-gray-100 px-3 py-1 text-sm hover:bg-gray-200"
>
編集
</a>
)}
{/* admin のみ削除ボタンを表示 */}
{hasRole(user, "admin") && (
<button
className="rounded bg-red-100 px-3 py-1 text-sm text-red-700 hover:bg-red-200"
>
削除
</button>
)}
</div>
);
}
Client Component でのロールガード
// components/RoleGuard.tsx
"use client";
type Props = {
role: string;
required: "admin" | "editor" | "viewer";
children: React.ReactNode;
fallback?: React.ReactNode;
};
const HIERARCHY = ["viewer", "editor", "admin"] as const;
export function RoleGuard({ role, required, children, fallback = null }: Props) {
const hasAccess =
HIERARCHY.indexOf(role as (typeof HIERARCHY)[number]) >=
HIERARCHY.indexOf(required);
return hasAccess ? <>{children}</> : <>{fallback}</>;
}
// 使用例
<RoleGuard role={user.role} required="editor">
<button>編集</button>
</RoleGuard>
<RoleGuard role={user.role} required="admin" fallback={<p>権限がありません</p>}>
<AdminPanel />
</RoleGuard>
forbidden() と UI 制御の使い分け
| 状況 | 手段 |
|---|---|
| ページ自体へのアクセスを完全に拒否 | forbidden() |
| ページは表示するが一部 UI を隠す | hasRole() 条件分岐 |
| Client Component でロール条件分岐 | RoleGuard コンポーネント |
ポイント
hasRoleにロール階層(viewer < editor < admin)を持たせることで、「editor 以上」という条件を単一の関数で表現できる- Server Component のガードは
forbidden()を使い、ページ単位でブロックする。UI 要素レベルの制御はhasRole()条件分岐で行い、責務を分ける RoleGuardを Client Component として実装することで、user.roleを props で受け取りツリー内で任意に再利用できる- Server Component ではロールチェックをレンダリング時に行えるため、Client Component よりも条件が確実に適用される。重要な制御は Server Component 側で行う
fallbackをnullにすることで、権限不足の場合に何も表示しないデフォルト動作を実現できる