2.4 페이지/컴포넌트 가드 훅 구현(usePermission)
This commit is contained in:
@@ -1,8 +1,16 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import prisma from "@/lib/prisma";
|
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 { 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({
|
const post = await prisma.post.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { status: "published" },
|
data: { status: "published" },
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { z } from "zod";
|
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() });
|
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);
|
const parsed = schema.safeParse(body);
|
||||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
||||||
const { pinned, order } = parsed.data;
|
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({
|
const post = await prisma.post.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { isPinned: pinned, pinnedOrder: pinned ? order ?? 0 : null },
|
data: { isPinned: pinned, pinnedOrder: pinned ? order ?? 0 : null },
|
||||||
|
|||||||
11
src/lib/auth.ts
Normal file
11
src/lib/auth.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
55
src/lib/rbac.ts
Normal file
55
src/lib/rbac.ts
Normal file
@@ -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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
[권한/역할(RBAC)]
|
[권한/역할(RBAC)]
|
||||||
2.1 역할·권한 시드(admin/editor/user) 추가 o
|
2.1 역할·권한 시드(admin/editor/user) 추가 o
|
||||||
2.2 권한 enum/매핑 정의(리소스/액션)
|
2.2 권한 enum/매핑 정의(리소스/액션) o
|
||||||
2.3 서버 권한 미들웨어 적용(API 보호 라우트 지정)
|
2.3 서버 권한 미들웨어 적용(API 보호 라우트 지정) o
|
||||||
2.4 페이지/컴포넌트 가드 훅 구현(usePermission)
|
2.4 페이지/컴포넌트 가드 훅 구현(usePermission)
|
||||||
2.5 권한 기반 UI 노출 제어(빠른 액션/관리자 메뉴)
|
2.5 권한 기반 UI 노출 제어(빠른 액션/관리자 메뉴)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user