fix: 로컬 네트워크 접근 시 로그인 불가 문제 해결

- 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 유지(입력 검증은 엄격하게)
This commit is contained in:
머니페니 2026-05-27 22:51:57 +09:00
parent e8eabe61ab
commit efbe0f5c3e
3 changed files with 55 additions and 12 deletions

View File

@ -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:

View File

@ -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;
}

View File

@ -1,13 +1,5 @@
const API_URL = process.env.NEXT_PUBLIC_API_URL || '';
async function hashPassword(password: string): Promise<string> {
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) {