diff --git a/src/app/api/posts/[id]/approve/route.ts b/src/app/api/posts/[id]/approve/route.ts index 3d46f06..4df9e5a 100644 --- a/src/app/api/posts/[id]/approve/route.ts +++ b/src/app/api/posts/[id]/approve/route.ts @@ -1,8 +1,16 @@ import { NextResponse } from "next/server"; import prisma from "@/lib/prisma"; +import { requirePermission } from "@/lib/rbac"; +import { getUserIdFromRequest } from "@/lib/auth"; -export async function POST(_: Request, context: { params: Promise<{ id: string }> }) { +export async function POST(req: Request, context: { params: Promise<{ id: string }> }) { const { id } = await context.params; + const userId = getUserIdFromRequest(req); + try { + await requirePermission({ userId, resource: "BOARD", action: "MODERATE" }); + } catch (e: any) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } const post = await prisma.post.update({ where: { id }, data: { status: "published" }, diff --git a/src/app/api/posts/[id]/pin/route.ts b/src/app/api/posts/[id]/pin/route.ts index d7f7abb..c3d4440 100644 --- a/src/app/api/posts/[id]/pin/route.ts +++ b/src/app/api/posts/[id]/pin/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; import prisma from "@/lib/prisma"; import { z } from "zod"; +import { requirePermission } from "@/lib/rbac"; +import { getUserIdFromRequest } from "@/lib/auth"; const schema = z.object({ pinned: z.boolean(), order: z.number().int().nullable().optional() }); @@ -10,6 +12,12 @@ export async function POST(req: Request, context: { params: Promise<{ id: string const parsed = schema.safeParse(body); if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); const { pinned, order } = parsed.data; + const userId = getUserIdFromRequest(req); + try { + await requirePermission({ userId, resource: "BOARD", action: "MODERATE" }); + } catch (e: any) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } const post = await prisma.post.update({ where: { id }, data: { isPinned: pinned, pinnedOrder: pinned ? order ?? 0 : null }, diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..bf82efb --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,11 @@ +// 간이 인증 헬퍼: 실제 세션/쿠키 연동 전 임시로 헤더에서 userId를 읽어옵니다. +export function getUserIdFromRequest(req: Request): string | null { + try { + const id = req.headers.get("x-user-id"); + return id && id.length > 0 ? id : null; + } catch { + return null; + } +} + + diff --git a/src/lib/rbac.ts b/src/lib/rbac.ts new file mode 100644 index 0000000..32ee010 --- /dev/null +++ b/src/lib/rbac.ts @@ -0,0 +1,55 @@ +import prisma from "@/lib/prisma"; + +// 권한 확인: userId가 주어지면 역할 → 권한 매핑(RolePermission)으로 검사 +export async function checkPermission(options: { + userId: string | null | undefined; + resource: "BOARD" | "POST" | "COMMENT" | "USER" | "ADMIN"; + action: "READ" | "CREATE" | "UPDATE" | "DELETE" | "MODERATE" | "ADMINISTER"; +}): Promise { + const { userId, resource, action } = options; + if (!userId) return false; + + const userRoles = await prisma.userRole.findMany({ + where: { userId }, + select: { roleId: true }, + }); + if (userRoles.length === 0) return false; + const roleIds = userRoles.map((r) => r.roleId); + const has = await prisma.rolePermission.findFirst({ + where: { + roleId: { in: roleIds }, + resource, + action, + allowed: true, + }, + select: { id: true }, + }); + if (has) return true; + // ADMIN.ADMINISTER 이면 모든 리소스/액션 허용 + const isAdmin = await prisma.rolePermission.findFirst({ + where: { + roleId: { in: roleIds }, + resource: "ADMIN", + action: "ADMINISTER", + allowed: true, + }, + select: { id: true }, + }); + return !!isAdmin; +} + +export async function requirePermission(options: { + userId: string | null | undefined; + resource: "BOARD" | "POST" | "COMMENT" | "USER" | "ADMIN"; + action: "READ" | "CREATE" | "UPDATE" | "DELETE" | "MODERATE" | "ADMINISTER"; +}): Promise { + const ok = await checkPermission(options); + if (!ok) { + const err = new Error("Forbidden"); + // @ts-expect-error attach status for route handlers + err.status = 403; + throw err; + } +} + + diff --git a/todolist.txt b/todolist.txt index 716d990..645ec50 100644 --- a/todolist.txt +++ b/todolist.txt @@ -7,8 +7,8 @@ [권한/역할(RBAC)] 2.1 역할·권한 시드(admin/editor/user) 추가 o -2.2 권한 enum/매핑 정의(리소스/액션) -2.3 서버 권한 미들웨어 적용(API 보호 라우트 지정) +2.2 권한 enum/매핑 정의(리소스/액션) o +2.3 서버 권한 미들웨어 적용(API 보호 라우트 지정) o 2.4 페이지/컴포넌트 가드 훅 구현(usePermission) 2.5 권한 기반 UI 노출 제어(빠른 액션/관리자 메뉴)