From d5a8efbb77d19fc8e8828b77f64417b9fad66eda Mon Sep 17 00:00:00 2001 From: nelee Date: Tue, 12 May 2026 23:36:10 +0900 Subject: [PATCH] =?UTF-8?q?fix(security):=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?API=20=EC=9D=B8=EC=A6=9D/=EA=B6=8C=ED=95=9C=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - checkAdminAuth 공유 유틸 추가 (src/lib/auth/adminAuth.ts) - admin/users GET, admin/users/role PATCH, admin/projects GET에 관리자 인증 추가 - admin/invitations GET/POST에 관리자 인증 추가 - invitations POST에서 invited_by를 body 대신 세션에서 추출하도록 수정 - announcements의 중복 checkAdminFnc 제거 후 공유 유틸로 대체 Co-Authored-By: Claude Sonnet 4.6 --- src/app/api/admin/invitations/route.ts | 14 +++++-- src/app/api/admin/projects/route.ts | 6 ++- src/app/api/admin/users/role/route.ts | 6 ++- src/app/api/admin/users/route.ts | 25 ++++++------ src/app/api/announcements/route.ts | 55 ++++---------------------- src/lib/auth/adminAuth.ts | 26 ++++++++++++ 6 files changed, 65 insertions(+), 67 deletions(-) create mode 100644 src/lib/auth/adminAuth.ts diff --git a/src/app/api/admin/invitations/route.ts b/src/app/api/admin/invitations/route.ts index ec2ae12..b5d2f89 100644 --- a/src/app/api/admin/invitations/route.ts +++ b/src/app/api/admin/invitations/route.ts @@ -1,8 +1,12 @@ import { NextResponse } from "next/server"; import { supabaseAdmin } from "@/lib/supabase/server"; +import { checkAdminAuth } from "@/lib/auth/adminAuth"; export async function GET() { try { + const auth = await checkAdminAuth(); + if (!auth.authorized) return auth.error; + const { data, error } = await supabaseAdmin .from("project_invitation_new") .select(` @@ -61,16 +65,18 @@ const formatted = data.map((row) => ({ export async function POST(req: Request) { try { + const auth = await checkAdminAuth(); + if (!auth.authorized) return auth.error; + const body = await req.json(); - const { invitation_type, // "service_only" | "project" email, - project_id, //프로젝트 초대일때만사용 - project_role, //프로젝트 초대일때만사용 - invited_by, // 관리자 user_id + project_id, // 프로젝트 초대일 때만 사용 + project_role, // 프로젝트 초대일 때만 사용 } = body; + const invited_by = auth.userId; //email, 초대타입 필수! if (!email || !invitation_type) { diff --git a/src/app/api/admin/projects/route.ts b/src/app/api/admin/projects/route.ts index 0a43fdf..c4c49b9 100644 --- a/src/app/api/admin/projects/route.ts +++ b/src/app/api/admin/projects/route.ts @@ -1,10 +1,12 @@ -// src/app/api/admin/projects/route.ts - import { NextResponse } from "next/server"; import { supabaseAdmin } from "@/lib/supabase/server"; +import { checkAdminAuth } from "@/lib/auth/adminAuth"; export async function GET() { try { + const auth = await checkAdminAuth(); + if (!auth.authorized) return auth.error; + const { data, error } = await supabaseAdmin .from("projects") .select("project_id, project_name") diff --git a/src/app/api/admin/users/role/route.ts b/src/app/api/admin/users/role/route.ts index aa19602..1f93ca2 100644 --- a/src/app/api/admin/users/role/route.ts +++ b/src/app/api/admin/users/role/route.ts @@ -1,8 +1,12 @@ import { NextResponse } from "next/server"; -import { supabaseAdmin } from "@/lib/supabase/server"; // service role +import { supabaseAdmin } from "@/lib/supabase/server"; +import { checkAdminAuth } from "@/lib/auth/adminAuth"; export async function PATCH(req: Request) { try { + const auth = await checkAdminAuth(); + if (!auth.authorized) return auth.error; + const { user_id, newRole } = await req.json(); if (!user_id || !newRole) { diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 699493c..b8c2e77 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -1,27 +1,26 @@ import { supabaseAdmin } from "@/lib/supabase/server"; import { NextResponse } from "next/server"; +import { checkAdminAuth } from "@/lib/auth/adminAuth"; - -//user 정보 조회 export async function GET() { - try { - const {data, error} = await supabaseAdmin - .from("users") - .select(`user_name,email,global_role,user_id`) + const auth = await checkAdminAuth(); + if (!auth.authorized) return auth.error; - if (error) { - console.error("Supabase error:", error); - return NextResponse.json({ error: "DB error" }, { status: 500 }); - } - - return NextResponse.json(data) + const { data, error } = await supabaseAdmin + .from("users") + .select(`user_name,email,global_role,user_id`); + if (error) { + console.error("Supabase error:", error); + return NextResponse.json({ error: "DB error" }, { status: 500 }); + } + return NextResponse.json(data); } catch (error) { console.error(error); return NextResponse.json({ error: "Server error" }, { status: 500 }); - } + } } diff --git a/src/app/api/announcements/route.ts b/src/app/api/announcements/route.ts index 47a7582..d656d8a 100644 --- a/src/app/api/announcements/route.ts +++ b/src/app/api/announcements/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; -import { getUnifiedAuthUser } from "@/lib/auth/unifiedAuth"; import { supabaseAdmin } from "@/lib/supabase/server"; +import { checkAdminAuth } from "@/lib/auth/adminAuth"; // ------------------------------------------------------ // 공통 에러 핸들러 @@ -25,39 +25,6 @@ async function handleRequest(fn: () => Promise) { } } -// ------------------------------------------------------ -// 권한 체크 -// ------------------------------------------------------ - -export async function checkAdminFnc() { - const authUser = await getUnifiedAuthUser(); - - if (!authUser.isAuthenticated || !authUser.userId) { - return { - authorized: false, - error: NextResponse.json( - { error: "로그인이 필요합니다." }, - { status: 401 } - ), - }; - } - - const isAdmin = authUser.role === "admin"; - if (!isAdmin) { - return { - authorized: false, - error: NextResponse.json( - { error: "관리자 권한이 필요합니다." }, - { status: 403 } - ), - }; - } - - const user_id = authUser.userId; - - return { authorized: true, user_id }; -} - // ------------------------------------------------------ // 공지사항 유효성 검사 @@ -164,22 +131,17 @@ export async function GET(request: Request) { export async function POST(request: Request) { return handleRequest(async () => { // 로그인, 관리자 여부 확인한 다음 - const permission = await checkAdminFnc(); - // 둘 중 하나라도 아니라면 false 또는 에러 즉시 반환 - if (!permission.authorized) return permission.error; + const auth = await checkAdminAuth(); + if (!auth.authorized) return auth.error; - // title, content, is_important를 가져온다 const body = await request.json(); - // 입력값 검증 - // 제목, 내용 없거나 제목 255자 초과 시 -> 에러 validateNoticeInput(body); const { data, error } = await supabaseAdmin .from("notices") - // insert()는 기본적으로 배열 형태의 rows를 받는다고 한다 .insert([ { - user_id: permission.user_id, + user_id: auth.userId, title: body.title.trim(), content: body.content.trim(), is_important: body.is_important || false, @@ -201,9 +163,8 @@ export async function POST(request: Request) { export async function PUT(request: Request) { return handleRequest(async () => { // 로그인, 관리자 여부 확인한 다음 - const permission = await checkAdminFnc(); - // 둘 중 하나라도 아니라면 false 또는 에러 즉시 반환 - if (!permission.authorized) return permission.error; + const auth = await checkAdminAuth(); + if (!auth.authorized) return auth.error; // /api/notices/:id 방식으로 추출하거나 쿼리스트링에서 가져와서 // 이 id 기준으로 update가 진행 @@ -243,8 +204,8 @@ export async function PUT(request: Request) { export async function DELETE(request: Request) { return handleRequest(async () => { - const permission = await checkAdminFnc(); - if (!permission.authorized) return permission.error; + const auth = await checkAdminAuth(); + if (!auth.authorized) return auth.error; const announcement_id = getAnnouncementId(request); diff --git a/src/lib/auth/adminAuth.ts b/src/lib/auth/adminAuth.ts new file mode 100644 index 0000000..ff2f012 --- /dev/null +++ b/src/lib/auth/adminAuth.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import { getUnifiedAuthUser } from "@/lib/auth/unifiedAuth"; + +type AdminAuthResult = + | { authorized: true; userId: string } + | { authorized: false; error: NextResponse }; + +export async function checkAdminAuth(): Promise { + const authUser = await getUnifiedAuthUser(); + + if (!authUser.isAuthenticated || !authUser.userId) { + return { + authorized: false, + error: NextResponse.json({ error: "로그인이 필요합니다." }, { status: 401 }), + }; + } + + if (authUser.role !== "admin") { + return { + authorized: false, + error: NextResponse.json({ error: "관리자 권한이 필요합니다." }, { status: 403 }), + }; + } + + return { authorized: true, userId: authUser.userId }; +}