From 3fa865a6e7b9b521bb1b1eace711311af6890a0e Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 08:41:42 +0900 Subject: [PATCH] =?UTF-8?q?fix(orders):=20user=20=EA=B8=B0=EC=A4=80=20rate?= =?UTF-8?q?=20limit=20+=20=EC=83=81=ED=92=88=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - checkRateLimit('orders:{user.id}', 60_000, 5) 인증 직후 적용 → 429 반환 - getProductById try/catch 추가 → DB 장애 시 500 '상품 조회에 실패했습니다' - lib/order-emails.ts sendOrderPaidEmail HTML 이스케이프 대상 없음 (해당 없음) Co-Authored-By: Claude Sonnet 4.6 --- app/api/orders/route.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/app/api/orders/route.ts b/app/api/orders/route.ts index 8345419..c753f8b 100644 --- a/app/api/orders/route.ts +++ b/app/api/orders/route.ts @@ -3,7 +3,7 @@ import { cookies } from 'next/headers'; import { createServerClient as createSSRClient } from '@supabase/ssr'; import { createAdminClient } from '@/lib/supabase/admin'; import { getProductById } from '@/lib/supabase/product-files'; -import { sanitizeStr } from '@/lib/security'; +import { sanitizeStr, checkRateLimit } from '@/lib/security'; import { sendOrderReceivedEmails } from '@/lib/order-emails'; export const runtime = 'nodejs'; @@ -26,6 +26,18 @@ export async function POST(request: Request) { return NextResponse.json({ error: '로그인이 필요합니다' }, { status: 401 }); } + // 1-b) Rate Limit: user 기준 분당 5회 + const rl = checkRateLimit(`orders:${user.id}`, 60_000, 5); + if (!rl.allowed) { + return NextResponse.json( + { error: '요청이 너무 잦습니다. 잠시 후 다시 시도해주세요' }, + { + status: 429, + headers: { 'Retry-After': String(Math.ceil(rl.retryAfterMs / 1000)) }, + }, + ); + } + // 2) body 검증 let body: unknown; try { @@ -46,7 +58,13 @@ export async function POST(request: Request) { // 3) 상품 조회 및 활성 상태 확인 const admin = createAdminClient(); - const product = await getProductById(admin, productId); + let product; + try { + product = await getProductById(admin, productId); + } catch (dbErr) { + console.error('[Orders] product lookup error:', dbErr); + return NextResponse.json({ error: '상품 조회에 실패했습니다' }, { status: 500 }); + } if (!product || !product.is_active) { return NextResponse.json({ error: '판매 중인 상품이 아닙니다' }, { status: 404 });