From efbe0f5c3e6dee3867d049002ccfb085de3c1296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A8=B8=EB=8B=88=ED=8E=98=EB=8B=88?= Date: Wed, 27 May 2026 22:51:57 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A1=9C=EC=BB=AC=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EC=A0=91=EA=B7=BC=20=EC=8B=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=B6=88=EA=B0=80=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - frontend: crypto.subtle(Secure Context 전용)을 Next.js Route Handler로 대체 - src/app/api/auth/login/route.ts 신규 생성 - Node.js crypto.createHash('sha256')로 서버사이드 해싱 - http://192.168.x.x 등 insecure HTTP 환경에서도 동작 - api.ts에서 클라이언트 사이드 hashPassword() 제거 - backend: UserResponse 응답 직렬화 시 500 에러 수정 - UserResponse.email을 EmailStr → str로 변경 - admin@local 처럼 TLD 없는 내부 이메일도 응답 가능 - UserCreate.email은 EmailStr 유지(입력 검증은 엄격하게) --- backend/app/schemas/user.py | 5 ++- frontend/src/app/api/auth/login/route.ts | 46 ++++++++++++++++++++++++ frontend/src/lib/api.ts | 16 +++------ 3 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 frontend/src/app/api/auth/login/route.ts diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 0969b2f..c27b79c 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -7,15 +7,18 @@ from pydantic import BaseModel, EmailStr class UserBase(BaseModel): username: str - email: EmailStr class UserCreate(UserBase): + """Input schema — strict EmailStr validation for user-supplied email.""" + email: EmailStr password: str class UserResponse(UserBase): + """Response schema — email is serialised as-is from DB (no re-validation).""" id: int + email: str created_at: datetime class Config: diff --git a/frontend/src/app/api/auth/login/route.ts b/frontend/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..3e7a59d --- /dev/null +++ b/frontend/src/app/api/auth/login/route.ts @@ -0,0 +1,46 @@ +/** + * Next.js Route Handler for login. + * + * Handles SHA-256 password hashing server-side so the login flow works + * in non-secure HTTP contexts (e.g. http://192.168.x.x) where the browser + * blocks window.crypto.subtle (Web Crypto API requires a Secure Context). + * + * This handler intercepts /api/auth/login before the next.config rewrite + * forwards it to the backend, computes the SHA-256 hash using Node.js + * built-in crypto, then proxies the request to the backend. + */ +import { createHash } from 'node:crypto'; +import { NextRequest, NextResponse } from 'next/server'; + +const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:8000'; + +export async function POST(request: NextRequest) { + let body: { username: string; password: string }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ detail: 'Invalid request body' }, { status: 400 }); + } + + const { username, password } = body; + + // Hash the raw password with SHA-256 (matches backend verify_password expectation) + const hashedPassword = createHash('sha256').update(password).digest('hex'); + + const backendResponse = await fetch(`${BACKEND_URL}/api/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password: hashedPassword }), + }); + + const data = await backendResponse.json(); + const response = NextResponse.json(data, { status: backendResponse.status }); + + // Forward the HttpOnly access_token cookie set by the backend + const setCookie = backendResponse.headers.get('set-cookie'); + if (setCookie) { + response.headers.set('set-cookie', setCookie); + } + + return response; +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index d85333f..68042d5 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,13 +1,5 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || ''; -async function hashPassword(password: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(password); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); -} - class ApiClient { private baseUrl: string; @@ -61,15 +53,17 @@ class ApiClient { } async login(username: string, password: string) { - const hashedPassword = await hashPassword(password); - + // Raw password is sent to the Next.js Route Handler (/api/auth/login), + // which hashes it server-side with Node.js crypto and forwards to the backend. + // This avoids dependency on window.crypto.subtle, which is only available + // in Secure Contexts (HTTPS or localhost) and fails over plain HTTP + LAN IP. const response = await fetch(`${this.baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', - body: JSON.stringify({ username, password: hashedPassword }), + body: JSON.stringify({ username, password }), }); if (!response.ok) {